開発プロセスと工数見積もり (PSP)
| PSP2.1 |
フェーズ |
予想時間 (分) |
実績時間 (分) |
| Planning |
計画 |
10 |
15 |
| Estimate |
工数見積もり |
10 |
5 |
| Development |
開発 |
20 |
30 |
| Analysis |
要件分析 |
20 |
20 |
| Design Spec |
設計文書作成 |
10 |
10 |
| Design Review |
設計レビュー |
10 |
10 |
| Coding Standard |
コーディング規約策定 |
10 |
10 |
| Design |
詳細設計 |
10 |
10 |
| Coding |
実装 |
20 |
20 |
| Code Review |
コードレビュー |
10 |
8 |
| Test |
テスト |
20 |
15 |
| Reporting |
報告 |
20 |
30 |
| Test Repor |
テストレポート |
20 |
15 |
| Size Measurement |
規模測定 |
10 |
10 |
| Postmortem |
振り返りと改善計画 |
10 |
20 |
|
合計 |
210 |
228 |
計算モジュールの設計と実装
1. アーキテクチャとコード構成
システムは主に以下の3つのコンポーネントで構成されています。
- トークン化モジュール: 入力テキストを単語(トークン)リストに分割します。
- ベクトル化モジュール: トークンリストを単語頻度ベクトルに変換します。
- 類似度計算モジュール: 生成されたベクトルに基づきコサイン類似度を算出します。
2. クラスおよびメソッド設計
TextSimilarityCalculator: システム全体のロジックを統括するメインクラスです。
ChineseTokenizer: jieba-analysisライブラリをラップし、中国語の形態素解析を行います。
computeCosineSimilarity: 2つのテキスト間の類似度を計算するコアメソッドです。
tokenizeText: 生のテキストデータを解析し、単語リストを返します。
constructFrequencyMap: 単語の出現頻度をマップ構造で構築します。
3. モジュール間の連携
computeCosineSimilarityメソッドが処理の中心となり、以下のフローで他のコンポーネントを呼び出します。
tokenizeTextを呼び出し、対象テキストをトークン化します。
constructFrequencyMapを用いて、各テキストの単語頻度マップを作成します。
- これらのマップを用いてコサイン類似度を算出し、その結果を返します。
なお、
tokenizeTextは
ChineseTokenizerに依存しており、
constructFrequencyMapの出力は
computeCosineSimilarityへの入力となります。
4. アルゴリズムの詳細
トークン化
中国語テキストの処理には
ChineseTokenizerを使用します。
- 入力: "我愛北京天安门"
- 出力: ["我", "愛", "北京", "天安门"]
処理効率と正確性のため、解析結果は小文字に統一されます。
単語頻度ベクトルの構築
データ構造として
Map<String, int[]>を採用します。
- キー (Key): 単語文字列
- 値 (Value): 長さ2の整数配列。インデックス0はテキストAの頻度、インデックス1はテキストBの頻度を示します。
この手法により、疎ベクトルの問題を回避し、メモリ効率を高めています。
コサイン類似度の計算
計算手順は以下の通りです。
- 内積 の算出: 単語ごとの頻度の積の総和 (sum(A_i * B_i))。
- ノルム (Norm) の算出: 各ベクトルの長さ (sqrt(sum(A_i^2)), sqrt(sum(B_i^2)))。
- 類似度の算出: 内積をノルムの積で除算 (dotProduct / (normA * normB))。
除算エラーを防ぐため、分母がゼロになる場合は事前にチェックを行います。
5. 実装上の特筆点
- モジュール性: トークン化、ベクトル化、計算ロジックを分離しているため、英語処理など他の言語への拡張が容易です。
- 効率性: ハッシュマップを用いることで、全テキストを1回走査するだけで頻度統計が完了します。計算量はトークン数をnとしてO(n)です。
計算モジュールの性能改善
1. 性能分析
- トークン化フェーズ: テキスト長をNとして計算量はO(N)です。2つのテキストを処理するため、合計O(N1 + N2)となります。
- ベクトル構築フェーズ: トークン総数をMとして、頻度カウントはO(M)で完了します。
- 類似度計算フェーズ: 辞書サイズ(異なる単語の数)をKとして、計算量はO(K)です。
2. 最適化アプローチ
Map<String, int[]>構造により、多次元配列を使用した従来のベクトル表現に比べ、不要な次元(頻度が0の次元)を保持しないため、計算リソースを大幅に節約できます。また、Jiebaによる高速な形態素解析ライブラリを選定することで、処理速度を確保しています。
計算モジュールの単体テスト
1. トークン化機能の検証
中国語の文字列を入力し、期待通りの単語リストが返されるか確認します。
@Test
public void testTokenizationProcess() {
ChineseTokenizer tokenizer = new ChineseTokenizer();
String sourceData = "我愛软件工程";
List<String> processedTokens = TextSimilarityCalculator.tokenizeText(tokenizer, sourceData);
List<String> expectedTokens = Arrays.asList("我", "愛", "软件工程");
Assertions.assertEquals(expectedTokens, processedTokens);
}
2. 頻度マップ構築の検証
異なるリストを入力し、各単語の頻度が正しくカウントされているか確認します。
@Test
public void testFrequencyMapConstruction() {
List<String> tokensA = Arrays.asList("apple", "banana", "apple");
List<String> tokensB = Arrays.asList("banana", "orange", "banana");
Map<String, int[]> frequencyMap = new HashMap<>();
TextSimilarityCalculator.constructFrequencyMap(tokensA, frequencyMap, 0);
TextSimilarityCalculator.constructFrequencyMap(tokensB, frequencyMap, 1);
Assertions.assertArrayEquals(new int[]{2, 0}, frequencyMap.get("apple"));
Assertions.assertArrayEquals(new int[]{1, 2}, frequencyMap.get("banana"));
Assertions.assertArrayEquals(new int[]{0, 1}, frequencyMap.get("orange"));
}
3. メイン処理(ファイルI/O)の検証
一時ファイルを作成し、コマンドライン引数を模倣してメインメソッドを実行、出力結果を検証します。
@Test
public void testMainExecution(@TempDir Path tempDir) throws IOException {
File sourceFile = tempDir.resolve("source.txt").toFile();
File targetFile = tempDir.resolve("target.txt").toFile();
File resultFile = tempDir.resolve("result.txt").toFile();
Files.write(sourceFile.toPath(), "我愛北京天安门".getBytes());
Files.write(targetFile.toPath(), "我愛北京天安门".getBytes());
String[] cliArgs = {
sourceFile.getAbsolutePath(),
targetFile.getAbsolutePath(),
resultFile.getAbsolutePath()
};
TextSimilarityCalculator.main(cliArgs);
String content = Files.readString(resultFile.toPath());
Assertions.assertEquals("100.00%", content.trim());
}
例外処理と堅牢性の確保
1. ファイル読み込みエラー処理
ファイルが存在しない、またはアクセス権限がない場合の処理を実装します。
@Test
void testFileReadException() {
String invalidPath = "missing_document.txt";
Exception thrown = Assertions.assertThrows(IOException.class, () -> {
TextSimilarityCalculator.readContentFromFile(invalidPath);
});
Assertions.assertTrue(thrown.getMessage().contains("指定されたファイルが見つかりません"));
}
2. 空入力に対する処理
空の文字列やトークンリストが渡された場合、NullPointer例外が発生しないようにガードします。
@Test
void testEmptyInputHandling() {
Map<String, int[]> mapData = new HashMap<>();
List<String> emptyList = Collections.emptyList();
TextSimilarityCalculator.constructFrequencyMap(emptyList, mapData, 0);
Assertions.assertTrue(mapData.isEmpty());
}
3. ゼロ除算の回避
両方のテキストが空である場合、類似度計算で除算エラーが発生しないよう、事前にベクトルのノルムをチェックします。
@Test
void testZeroVectorSimilarity() {
String blankText1 = "";
String blankText2 = "";
double result = TextSimilarityCalculator.computeCosineSimilarity(blankText1, blankText2);
Assertions.assertEquals(0.0, result, 0.0001);
}