
MapReduceプログラミングモデルをJavaで実装し 日本語テキストの単語出現頻度を求める
2020.08.14
目次
MapReduceプログラミングモデルをJavaで実装し日本語テキストの単語出現頻度を求めてみる
MapReduce とは、簡単に言うと「膨大なデータを処理するためのプログラミングモデル及びその実装」のことです。
もともと MapReduce は、Google社の Jeffrey Dean と Sanjay Ghemawat が執筆した論文に紹介されていました。
現在では、Google だけではなく、多数の大手会社によって導入されています。
一部ではありますが、導入している会社はこんなビッグネームばかりです。
- Apache Software Foundation
- Cloudera
- Hortonworks
- IBM
- Oracle
- Amazon
- Microsoft
- MapR
- Hitachi
最近のビッグデータの時代では、MapReduce が基盤技術のひとつとなっています。
MapReduce の利点
MapReduce の利点の1つとしては、比較的にシンプルな手法で、膨大なデータの並列分散処理が行うことが出来ることです。
MapReduce のプログラミングモデル
MapReduce は、「Mapフェーズ」と「Reduceフェーズ」から成り立っています。
Dean氏と Ghemawat氏の論文では、MapReduce のプログラミングモデルを説明するために、多大のテキストドキュメントにおける単語の出現頻度を求める例が挙げられました。
対象となるテキストは、もちろん「英文」です。
このサンプルケースにおいて、MapReduce のプログラミングモデルは以下のような疑似コード(pseudo-code)で実装されました。
1 2 3 4 5 6 7 8 9 10 11 12 13 | map(String key, String value): // key: document name // value: document contents for each word w in value: EmitIntermediate(w, "1"); reduce(String key, Iterator values): // key: a word // values: a list of counts int result = 0; for each v in values: result += ParseInt(v); Emit(AsString(result)); |
疑似コードに基づいて MapReduce プログラミングモデルを実装してみる!
今回は、この疑似コードに基づいて、MapReduce プログラミングモデルを実装し、複数の日本語テキストドキュメントから単語の出現頻度を求めてみたいと思います!
こちらの記事もオススメ!
MapReduce のプログラミングモデルをJavaで実装
対象データは日本語のテキスト文章ですので、日本語の「自然言語処理(Natural Language Processing)ライブラリー」が必要になります。
そこで、今回は使うのは「Kuromoji」という、日本語自然言語処理 Java ライブラリーです。
【Kuromojiダウンロードサイト】
https://github.com/atilika/kuromoji/downloads
Kuromojiの詳細
Kuromoji の詳細については、以下の Atilika のウェーブページをご参照ください!
【Atilika】
https://www.atilika.org/
日本語のストップワードリスト
その他に、日本語の文章から「助詞」や「非常に一般な単語かつ文章の文脈にあまり影響を及ばない単語(stopword)」を取り除くために、日本語のストップワードリストも必要です。
これは Github からダウンロードすることが出来ます。
【Github(ストップワードリスト)】
https://github.com/stopwords-iso/stopwords-ja/blob/master/stopwords-ja.txt
開発環境
今回の執筆者の環境は、以下の通りとなっています。
- IDE: IntelliJ IDEA
- JDK: Oracle Java 8
- OS: MacOS Catalina
プロジェクトの構成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | mapreduce |_input |_日本語テキスト.txt // 3個のテキストファイル |_lib |_kuromoji-0.7.7.jar //日本語自然言語処理ライブラリー |_src |_common //入力テキストファイルを読み込むためのJavaコードなど |_StopwordReader.java |_stopwords_jp.txt //日本語のストップワードリスト |_TextFileReader.java |_MapReduceWordFreq //MapReduceの実装Javaコード |_MapReduceWordFreq.java |_Mapper.java |_MapOut.java |_Reducer.java |_ReduceIn.java |_Result.java |_WordFreq //MapReduceを使わずに、単語出現頻度を求めるプログラム |_WordFreq.java |
MapReduce:Mapフェーズ
「Mapフェーズ」では、inputディレクターから読み込まれた3個の日本語テキストファイルを、Mapper.java の map(String key, String value) 関数に渡します。
- 引数 key は、テキストファイルのファイル名です。
- 引数 value は、テキストファイルの内容です。
文を構造する全ての単語を取得し、トークンとして扱う
まず、regex を用いて、テキスト文章から単語でないものを削除しましょう。
そして、Kuromoji ライブラリーの Tokenizer を適用し、テキスト文章のトークン化(Tokenization)を行います。
つまり、文を構造する全ての単語を取得し、トークン(Token)として扱います。
そして、得られた全ての単語を emitIntermediate(String key, Integer value) 関数に渡します。
- 引数 key はひとつの単語です。
- 引数 value は Integer の 1 です。
ストップワードである単語を除く
emitIntermediate(String key, Integer value) 関数では、Mapフェーズの出力(MapOutオブジェクト)が生成されます。
ここで、ストップワードのリストを用いて、ストップワードである単語を除きます。
MapOut.javaクラスは String key と Integer value の変数から成り立ちます。
変数 key はひとつの単語を格納し、 value は数値の1を格納します。
MapOut.java のコード
MapOut.java のコードは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 | package MapReduceWordFreq; public class MapOut { //Mapperの出力 public String key; //ひとつの単語を格納 public Integer value; //数字の1を格納 public MapOut(String key, Integer value){ this.key = key; this.value = value; } } |
Mapフェーズのテキストデータ処理は、Mapper.javaクラスにて実装しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | package MapReduceWordFreq; import common.StopwordReader; import org.atilika.kuromoji.Token; import org.atilika.kuromoji.Tokenizer; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Mapper { private List<String> stopwordList; public List<MapOut> mapOuts; public Mapper() throws IOException { stopwordList = new StopwordReader().read(); mapOuts = new ArrayList<>(); } public void map(String key, String value) { //key : テキストファイル名、value: テキストファイルの内容 System.out.println("============================="); System.out.println("Mapper --> list(key, value)"); System.out.println("============================="); Map<String, Integer> kv = new HashMap<>(); Tokenizer tokenizer = Tokenizer.builder().build(); value = value.replaceAll("\\P{LD}+", ""); List<Token> tokenList = tokenizer.tokenize(value); for (Token token:tokenList){ String word = token.getSurfaceForm(); emitIntermediate(word, 1); } } public void emitIntermediate(String key, Integer value){ // key: ひとつの単語、value : 数字の1 if (!stopwordList.contains(key)){ System.out.println(key + ":" + value.toString()); mapOuts.add(new MapOut(key, value)); } } } |
MapReduce:Reduceフェーズ
「Reduceフェーズ」では、Mapフェーズの出力(MapOutオブジェクト)を、Reducer.java の reduce(String key, List<Integer> values) 関数に渡します。
reduce(String key, List<Integer> values) 関数の引数 key は、ひとつの単語で、引数 values は数値1からなるリスト(List[1,1,1,...])です。
MapOut のデータ構造が、 reduce(String key, List<Integer> values) 関数の引数に合わないため、そのままデータを渡すことが出来ません。
ここで、ReduceIn.java の generate(List<MapOut> mapOuts) 関数で MapOut のデータ構造を ReduceIn のデータ構造に変換します。
ReduceIn.java のコード
ReduceIn.java のコードは、以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package MapReduceWordFreq; import java.util.ArrayList; import java.util.List; public class ReduceIn { //Reducerの入力データ構造 public String key; //ひとつの単語 public List<Integer> values; //list[1,1,1,1,...] public ReduceIn() { key = new String(); values = new ArrayList<>(); } public List<ReduceIn> generate(List<MapOut> mapOuts) { //Reducerの入力データ構造を生成します。 List<ReduceIn> reduceIns = new ArrayList<>(); for (MapOut mapOut:mapOuts){ boolean newKey = true; for (ReduceIn reduceIn:reduceIns){ if(reduceIn.key.equals(mapOut.key)){ newKey = false; reduceIn.values.add(mapOut.value); break; } } if (newKey){ ReduceIn reduceIn = new ReduceIn(); reduceIn.key = mapOut.key; reduceIn.values.add(mapOut.value); reduceIns.add(reduceIn); } } return reduceIns; } } |
Reduceフェーズが開始
それから、変換されたデータを reduce(String key, List<Integer> values) に渡して、Reduceフェーズが開始されます。
これによって、各単語の出現頻度をカウントされ、Result が出力されます。
Resultクラスは、「key(単語)」と「count(出現頻度)」という2つのメンバーから成り立っています。
Reducer.java と Result.java のコード
Reducer.java と Result.java のコードは、それぞれ以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package MapReduceWordFreq; import java.util.List; public class Reducer { //Reduceフェーズ private Result result; public Reducer(){ result = new Result(); } public Result reduce(String key, List<Integer> values){ Integer count = 0; for (Integer i:values){ count += i; } result.key = key; //ひとつの単語 result.count = count; //単語の出現頻度 return result; } } |
1 2 3 4 5 6 | package MapReduceWordFreq; public class Result { public String key; //単語 public Integer count; //出現頻度 } |
これで、MapフェーズとReduceフェーズの準備が完了しました!
MapReduce の実行
MapReduce を実行してみましょう!
MapReduceWordFreq.java の main(String[] args) で、日本語テキスト文章の単語出現頻度を求める MapReduce を実行します。
ここで先ず、入力ファイルを TextFileReader で読み込んで、それから Mapper と Reducer を実行します。
MapReduceWordFreq.java のコード
MapReduceWordFreq.java のコードは、次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package MapReduceWordFreq; import common.TextFileReader; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class MapReduceWordFreq { public static void main(String[] args) throws IOException { //テキストファイルの読み込み TextFileReader textFileReader = new TextFileReader(); Map<String, String> textFiles = textFileReader.readFiles(); //Mapフェーズの実行 Mapper mapTask = new Mapper(); List<MapOut> mapOuts = new ArrayList<>(); textFiles.forEach((k,v)->{ mapTask.map(k,v); mapOuts.addAll(mapTask.mapOuts); }); //Reduceフェーズの実行 Reducer reduceTask = new Reducer(); List<ReduceIn> reduceIns = new ReduceIn().generate(mapTask.mapOuts); System.out.println("============================="); System.out.println("Reducer(key, values)"); System.out.println("============================="); Map<String, Integer> results = new HashMap<>(); for (ReduceIn in: reduceIns){ System.out.println(in.key + ":" + in.values.toString()); Result result = reduceTask.reduce(in.key, in.values); int count = results.containsKey(result.key) ? results.get(result.key) : 0; results.put(result.key, count + result.count); } System.out.println("============================="); System.out.println("Reducer Results --> word:count"); System.out.println("============================="); for (Map.Entry<String, Integer> e:results.entrySet()){ System.out.println(e.getKey() + ":" + e.getValue()); } } } |
実行結果
============================= Mapper --> list(key, value) ============================= NHK:1 大河ドラマ:1 麒麟:1 くる:1 主人公:1 明智:1 光秀:1 所有:1 脇差し:1 名刀:1... 省略 ============================= Mapper --> list(key, value) ============================= 新型:1 コロナ:1 ウイルス:1 感染:1 拡大:1 受け:1 日本:1 政府:1 きょう:1 閣議:1... 省略 ============================= Mapper --> list(key, value) ============================= 北海道:1 札幌:1 市:1 新た:1 7:1 人:1 新型:1 コロナ:1 ウイルス:1 感染:1... 省略 ============================= Reducer(key, values) ============================= NHK:[1] 大河ドラマ:[1] 麒麟:[1] くる:[1] 主人公:[1] 明智:[1, 1, 1, 1] 光秀:[1, 1, 1, 1] 所有:[1, 1, 1, 1] 脇差し:[1, 1] 名刀:[1, 1]... 省略 ============================= Reducer Results --> word:count ============================= 刀:1 マカオ:1 市:5 討っ:1 者:2 分:2 万:2 滞在:1 上:1 部分:1 午前:2 成瀬:3 子孫:1 話し:1 くる:1 明智:4 発給:2 14:1 15:1 およそ:3... 省略Process finished with exit code 0 |
MapReduce の実行は成功しました!
MapReduce を使わずに簡単に求める方法とは?
でも、ちょっと待って。
単語の出現頻度を求める為なら、もっと簡単な方法があるのでは!?
その通りです。
以下のコード(WordFreq.java)で MapReduce を使わずに、より簡単な方法で単語の出現頻度を求めることが出来ます!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package WordFreq; import common.StopwordReader; import common.TextFileReader; import org.atilika.kuromoji.Token; import org.atilika.kuromoji.Tokenizer; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class WordFreq { private Tokenizer tokenizer; private List<String> stopwordList; private Map<String, Integer> wordFreqMap; public WordFreq() throws IOException { tokenizer = Tokenizer.builder().build(); stopwordList = new StopwordReader().read(); wordFreqMap = new HashMap<>(); } public void calculate(List<String> text){ List<Token> tokenList = new ArrayList<>(); text.forEach(item->{ item = item.replaceAll("\\P{LD}+", ""); tokenList.addAll(tokenizer.tokenize(item)); }); for (Token token:tokenList){ String currentWord = token.getSurfaceForm(); if (!stopwordList.contains(currentWord)){ int wordCount = wordFreqMap.containsKey(currentWord)? wordFreqMap.get(currentWord) : 0; wordFreqMap.put(currentWord, wordCount + 1); } } } public static void main(String[] args) throws IOException { WordFreq wordFreq = new WordFreq(); TextFileReader textFileReader = new TextFileReader(); List<String> text = new ArrayList<>(textFileReader.readFiles().values()); wordFreq.calculate(text); for (Map.Entry<String, Integer> kv: wordFreq.wordFreqMap.entrySet()){ System.out.println(kv.getKey() + ":" + kv.getValue()); } } } |
しかし、多数のファイルからなる膨大なテキストデータを処理する時に、WordFreq.java のような手法を用いると、処理スピードはマシーンの計算能力に制限されます。
それに対し MapReduce は、その膨大なデータを分割し、分散システム上で並列的に処理しますので、マシーンの数を増やすだけで、データ処理のスピードをどんどん向上させることが出来ます。
コードの追加
最後に、本記事の MapReduce 実装に用いた入力テキストファイルを読み込むコード(TextFileReader.java)と、日本語ストップワードファイルを読み込むコード(StopwordReader.java)を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package common; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; public class TextFileReader { private static final String DIR_NAME = "input/"; public Map<String, String> readFiles() throws IOException { Map<String, String> textFiles = new HashMap<>(); List<String> fileNames = getFileNames(); fileNames.forEach(item->{ BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(new FileInputStream(item))); String line = null; String content = ""; while ((line = br.readLine()) != null){ content += line; } textFiles.put(item, content); br.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }); return textFiles; } private List<String> getFileNames(){ List<String> result = new ArrayList<>(); try (Stream<Path> walk = Files.walk(Paths.get(DIR_NAME))) { result = walk.map(x -> x.toString()) .filter(f -> f.endsWith(".txt")).collect(Collectors.toList()); } catch (IOException e) { e.printStackTrace(); } return result; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package common; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; public class StopwordReader { public ArrayList<String> read() throws IOException { ArrayList<String > stwList = new ArrayList<>(); BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("src/common/stopwords_jp.txt"))); String line = null; while ((line = br.readLine()) != null){ stwList.add(line); } br.close(); return stwList; } } |
さいごに
MapReduce プログラミングモデルを実装し、日本語テキストドキュメントから単語の出現頻度を求めてみました!
また、MapReduce を使わずに簡単に求める方法も、併せてご紹介いたしました。
ご自身の環境に合わせて使い分けてみてください!
(株)ライトコードは、WEB・アプリ・ゲーム開発に強い、「好きを仕事にするエンジニア集団」です。
Javaでのシステム開発依頼・お見積もりはこちらまでお願いします。
また、Javaを扱えるエンジニアを積極採用中です!詳しくはこちらをご覧ください。
※現在、多数のお問合せを頂いており、返信に、多少お時間を頂く場合がございます。
こちらの記事もオススメ!
ライトコードよりお知らせ






一緒に働いてくれる仲間を募集しております!
ライトコードでは、仲間を募集しております!
当社のモットーは「好きなことを仕事にするエンジニア集団」「エンジニアによるエンジニアのための会社」。エンジニアであるあなたの「やってみたいこと」を全力で応援する会社です。
また、ライトコードは現在、急成長中!だからこそ、あなたにお任せしたいやりがいのあるお仕事は沢山あります。「コアメンバー」として活躍してくれる、あなたからのご応募をお待ちしております!
なお、ご応募の前に、「話しだけ聞いてみたい」「社内の雰囲気を知りたい」という方はこちらをご覧ください。
ライトコードでは一緒に働いていただける方を募集しております!
採用情報はこちら書いた人はこんな人

IT技術2021.03.02TypeScriptの型を問題形式で学べる「type-challenges」とは?
IT技術2021.03.01シスコルータのコンフィグ作成をPythonで自動化してみた!
IT技術2021.02.23【Unity】ARFoundation入門~機能解説から平面検知の実装まで~
IT技術2021.02.22Swiftでguardを使うメリットと使い方をご紹介!