テキスト類似度判定システムの設計と実装:Jiebaとコサイン類似度の応用

開発プロセスと工数見積もり (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メソッドが処理の中心となり、以下のフローで他のコンポーネントを呼び出します。
  1. tokenizeTextを呼び出し、対象テキストをトークン化します。
  2. constructFrequencyMapを用いて、各テキストの単語頻度マップを作成します。
  3. これらのマップを用いてコサイン類似度を算出し、その結果を返します。
なお、tokenizeTextChineseTokenizerに依存しており、constructFrequencyMapの出力はcomputeCosineSimilarityへの入力となります。

4. アルゴリズムの詳細

トークン化 中国語テキストの処理にはChineseTokenizerを使用します。
  • 入力: "我愛北京天安门"
  • 出力: ["我", "愛", "北京", "天安门"]
処理効率と正確性のため、解析結果は小文字に統一されます。 単語頻度ベクトルの構築 データ構造としてMap<String, int[]>を採用します。
  • キー (Key): 単語文字列
  • 値 (Value): 長さ2の整数配列。インデックス0はテキストAの頻度、インデックス1はテキストBの頻度を示します。
この手法により、疎ベクトルの問題を回避し、メモリ効率を高めています。 コサイン類似度の計算 計算手順は以下の通りです。
  1. 内積 の算出: 単語ごとの頻度の積の総和 (sum(A_i * B_i))。
  2. ノルム (Norm) の算出: 各ベクトルの長さ (sqrt(sum(A_i^2)), sqrt(sum(B_i^2)))。
  3. 類似度の算出: 内積をノルムの積で除算 (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);
}

9月4日 03:25 投稿