0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LIPS Schemeを使ってみました9(言語処理100本ノック 1章)

Last updated at Posted at 2026-01-02

はじめに

初荷Lips馬3_15.png

前回の言語処理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)すると次のようになります。

lng100_1-5(1).png

"私は学生です"を文字単位で2文字ずつn-gram(bi-gram)すると次のようになります。

lng100_1-5(2).png

今回のお題は上記のことができればよいわけです。
では、以下にお題の解答プログラムを見てみましょう。

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投稿になりました。
本年もご指導ご鞭撻のほどよろしくお願いいたします。🙇  

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?