はじめに
前回の言語処理100ノックの続きです。
今回はN-gramだけです。
コード作成はJSでプログラムイメージを作ってLipsに変換しました。
出回っている解答サイトのコードは短く簡単ですが
N-gramって何ですかというレベルからのチャレンジでした。
[変更履歴]
2026/01/02 初稿
2026/01/03 「(1) Lipsのvectorを使った場合」追加
お題 「05. ngram」
与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.
N-gramとは
文章は一つ以上の単語の並びでできていますね。単語は文字列でできています。
という前提で私は以下のように理解しました。
N-gramは一つの文章から任意の単語数または文字数を1塊ずつシフトしながら抜き出す操作のことです。
N-gramは抽出する塊のサイズによって1-gram(uni-gram)、3-gram(tri-gram)という単位があります。(理論的には4以上も可能です)
注意する点があります。抽出するサイズの単位を何にするかです。
- 単語(word)単位 (対象:英語のように単語単位で分かち書きできることば)
文字列をリスト化する前処理が必要になります。 - 文字(char)単位 (対象:日本語など単語単位で分かち書きできないことば)
データ形式がリストであれば文字列化等の前処理が必要になります。
"I am an NLPer"を単語単位で2語ずつn-gram(bi-gram)すると次のようになります。
"私は学生です"を文字単位で2文字ずつn-gram(bi-gram)すると次のようになります。
今回のお題は上記のことができればよいわけです。
では、以下にお題の解答プログラムを見てみましょう。
JSコード
JSはArray.sliceのおかげで簡単にできました。(解答サイトのpythonコードをgeminiで変換してもらっただけですが)
/**
* n-gramを作成する関数
* @param {number} n - 区切り数
* @param {string|string[]} data - 文字列または単語の配列
* @returns {any[]} n-gramのリスト
*/
function ngram(n, data) {
const lis = [];
for (let i = 0; i < data.length - n + 1; i++) {
lis.push(data.slice(i, i + n));
}
return lis;
}
// 単語bi-gram:単語単位でN-gram処理する
function ngramByWords(n, text) {
return ngram(n, text.split(" "))
}
// 文字bi-gram:文字単位でN-gram処理する
function ngramByString(n, text) {
return ngram(n, text)
}
const text1 = "I am an NLPer";
const text2 = "私は学生です";
console.log("単語bi-gram:", ngramByWords(2, text1));
console.log("文字bi-gram(J):", ngramByString(2, text2));
console.log("文字bi-gram(E):", ngramByString(2, text1));
// 出力:
// 単語bi-gram: [ [ 'I', 'am' ], [ 'am', 'an' ], [ 'an', 'NLPer' ] ]
// 文字bi-gram(J): [ '私は', 'は学', '学生', '生で', 'です' ]
// 文字bi-gram(E): [
// 'I ', ' a', 'am', 'm ', ' a', 'an',
// 'n ', ' N', 'NL', 'LP', 'Pe', 'er'
// ]
Lipsコード
(1) Lipsのvectorを使った場合
LipsのvectorはJSのArrayで実装されているのでJSコードをほぼ直訳できます。
;; n-gramを作成関数
(define (ngram n data)
(let (
(vec (list->vector data)) ; 処理したい文をベクター化する
(n_loop (- (length data) n)) ; 単語数または文字数
)
(let loop (
(i 0) ; ループカウンター
(acc '()) ; 抽出結果
)
(if (> i n_loop)
(reverse acc) ; 結合データを反転させて終わり
(loop
(+ i 1) ; カウントアップ
(cons (vec.slice i (+ i n)) acc) ; slice結果結合 (JSのdata.pushに該当)
)
)
) ; ----- end of (let loop (i) (acc)) ----
) ; ----- end of (let (vec) (n_loop)) ----
)
;; 単語bi-gram
(define (ngram-by-words n text)
;; スペースで区切って単語単位のリストにする
(ngram n (string-split " " text))
)
;; 文字bi-gram
(define (ngram-by-string n text)
;; 文字列をcar/cdr関数で扱えるようにするため文字リスト化する
(ngram n (string->list text))
)
(define text1 "I am an NLPer")
(define text2 "私は学生です")
;; テスト実行
(display "単語bi-gram: ")
(display (ngram-by-words 2 text1))
(newline)
(display "文字bi-gram(J): ")
(display (ngram-by-string 2 text2))
(newline)
(display "文字bi-gram(E): ")
(display (ngram-by-string 2 text1))
(newline)
;; 実行結果
;; 単語bi-gram: ((I am) (am an) (an NLPer))
;; 文字bi-gram(J): ((私 は) (は 学) (学 生) (生 で) (で す))
;; 文字bi-gram(E): ((I ) ( a) (a m) (m ) ( a) (a n) (n ) ( N) (N L) (L P) (P e) (e r))
(2) 普通のScheme風コード
sub-sequenceがコア処理になります。JSのArray.sliceに相当する関数です。
;; n-gramを作成する共通関数
(define (ngram n data)
(let (
(len (length data)) ; リストサイズ
;; (len2 (- len n)) ; 切り出しサイズ(N)
)
;; 切り出しサイズ分ループしてN語(またはN字)単位のリストを積み上げる(consする)
(let loop (
(i 0) ; ループカウンター
(acc '()) ; N-gram塊のリスト(戻り値)
)
(if (> i (- len n)) ;(> 0 1)->(> 1 2)->(> 2 2)...
; ex.(0,1)->(I am),(1,2)->(am an),(2,2)->(an NLPer)
(reverse acc) ; 戻り値(acc)
(loop (+ i 1) (cons (sub-sequence data i (+ i n)) acc)))
)
)
)
;; リストまたは文字列から部分シーケンスを抽出する(JSのArray.slice該当機能)
(define (sub-sequence data start end)
(if (string? data) ;対象がリスト操作か文字列操作かを判定する
(substring data start end) ; trueで文字単位で抽出する
(let loop ( ; falseであればあれば単語単位で抽出する
(curr data)
(i 0)
(res '())
)
(cond
((= i end) (reverse res))
((>= i start) (loop (cdr curr) (+ i 1) (cons (car curr) res)))
(else (loop (cdr curr) (+ i 1) res))
)
)
)
)
;; 単語bi-gram
(define (ngram-by-words n text)
;; スペースで区切って単語単位のリストにする
(ngram n (string-split " " text)))
;; 文字bi-gram
(define (ngram-by-string n text)
;; 文字列をcar/cdr関数で扱えるようにするため文字リスト化する
(ngram n (string->list text))
)
;; テストデータ
(define text1 "I am an NLPer")
(define text2 "私は学生です")
;; テスト出力
(display "単語bi-gram: ")
(display (ngram-by-words 2 text1))
(newline)
(display "文字bi-gram(J): ")
(display (ngram-by-string 2 text2))
(newline)
(display "文字bi-gram(E): ")
(display (ngram-by-string 2 text1))
(newline)
;; 出力結果
;; 単語bi-gram: ((I am) (am an) (an NLPer))
;; 文字bi-gram(J): ((私 は) (は 学) (学 生) (生 で) (で す))
;; 文字bi-gram(E): ((I ) ( a) (a m) (m ) ( a) (a n) (n ) ( N) (N L) (L P) (P e) (e r))
参考
終わりに
年が明けて2026年がはじまりました。9回目のqiita投稿になりました。
本年もご指導ご鞭撻のほどよろしくお願いいたします。🙇


