Chapter 4 · Applied Natural Language Processing
A bake-off, in the engineering sense: several candidate methods, one task, one protocol, and the same measuring stick for all of them.
LogisticRegression classC = 1.0: TF-IDF rows arrive with norm 1.0, the averaged vectors are not normalised — the same C need not mean the same regularisationThe results compare these concrete setups of the methods; we do not read the gap between them as the effect of the representation family alone.
A binary sentiment task: 235,165 Turkish product reviews, labelled positive or negative.
Three reasons for choosing it:
Barmanbay 2020 — Turkish product reviews
drop_duplicates keeps the first row and positive reviews come first in this file, so all 77 stayed with their positive labelrows is a digest of the selected row positions: the same 12 characters mean the same rows; the class totals do not show thatPredict “positive” for every review. Never look at the text at all.
The rule reaches 93.68% accuracy without using the review text at all, because 93.68% of the test set is positive — and it finds none of the 632 negative reviews.
\[\text{precision} = \frac{TP}{TP + FP}, \qquad \text{recall} = \frac{TP}{TP + FN}, \qquad F_1 = \frac{2\,TP}{2\,TP + FP + FN}\]
\[\text{macro-}F_1 = \frac{1}{K}\sum_{k=1}^{K} F_1^{(k)}\]
Macro-F1 is the equally weighted average of the per-class F1 scores. Accuracy weights examples equally; macro-F1 weights classes equally.
average= |
what it computes | the majority rule scores |
|---|---|---|
"binary" (the default for two classes) |
the F1 of class 1 only — here the majority positive class | 96.74 |
"micro" |
pools all the counts before dividing; here it equals accuracy | 93.68 |
"weighted" |
weights each class’s F1 by its number of examples | 90.62 |
"macro" |
weights the classes equally | 48.37 |
class_weight="balanced") are separate design decisionsIn this chapter macro-F1 is the main summary score for comparing the methods, and accuracy is reported alongside it for context. Macro-F1 is not a universal answer: recall on the negative class, precision at a given false-alarm budget, or expected business cost can be more direct decision metrics.
str.lower() implements Unicode’s default case mapping and does not change with the locale: i for I, and i plus a combining dot (U+0307) for İCase conversion is language-dependent, and a language-specific lowercasing policy is part of the preprocessing and tokeniser contract.
â, î, û and combining marks split a token!!!, 5/5 or an emoji can carry sentiment; this experiment does not model them, and the effect on the score was not measuredtr_lower assumes correct spelling: typed on an English keyboard, IYI may well mean “İYİ”, and tr_lower then produces ıyı, a new errorWhat ASCII-typed text needs is the restoration of Turkish characters (deasciification); the details are in the Turkish NLP chapter.
min_df=2 leaves 30,195 columns: min_df counts document frequency, not corpus frequency| prefix | distinct forms | examples |
|---|---|---|
kulla |
895 | kullanışlı, kullanıyorum, kullandım |
bekle |
285 | beklediğimden, beklediğim, beklemiyordum |
çalış |
247 | çalışıyor, çalışma, çalışan |
çıkar |
209 | çıkarıyor, çıkar, çıkartıyor |
düşün |
195 | düşünüyorum, düşünmeden, düşünenlere |
kullanışlı does not transfer by itself to kullanıyorumNot 895 inflections of one stem: inflections, derivations and spelling variants together. A five-character prefix is not stem or morpheme analysis.
| row | vectoriser | vocabulary threshold and tokens | columns |
|---|---|---|---|
| bag of words | CountVectorizer |
min_df=2, word pattern |
30,195 |
| tf-idf | TfidfVectorizer, sublinear_tf=True |
min_df=2, word pattern |
30,195 |
| tf-idf 1-2 gram | the same, with ngram_range=(1, 2) |
min_df=2, word pattern |
101,659 |
| tf-idf char_wb 3-5 | analyzer="char_wb", ngram_range=(3, 5) |
min_df=3; token_pattern is not used |
94,939 |
preprocessor=tr_lower, lowercase=False: otherwise scikit-learn calls Python’s str.lower()LogisticRegression(max_iter=2000, class_weight="balanced", random_state=42)The same input through the two analyzers: "5/5 harika!! ürün çok i̇yi 👍"
| analyzer | features | what happened |
|---|---|---|
analyzer="word", token_pattern=[a-zçğıöşü]+ |
['harika', 'ürün', 'çok', 'i', 'yi'] |
5/5, !!, the emoji are gone; the combining dot split “i̇yi” in two |
analyzer="char_wb", ngram_range=(3, 5) |
[' 5/', '5/5', '/5 ', ' ha', 'har', 'ari', ...] |
digits, the slash, the exclamation marks and the spaces all survive as features |
token_pattern is used only when analyzer="word"; char_wb ignores it entirely and runs its own character streammin_df=3 where the three word schemes use min_df=2: a defensible choice, but it was not sweptThe score difference of the character row may reflect this preprocessing choice along with the character features.
lowercase=False, round 1’s table came out byte-identical: not a single uppercase character among the 37,076,865 characters of the 233,508 de-duplicated reviewsitavsiye splits into tavsi + ye; deleting the mark takes the 30,195-column vocabulary down to 29,823: 595 columns exist only because of this splitting| result | tokens under [a-zçğıöşü]+ |
||
|---|---|---|---|
'İYİ'.lower() |
'i̇yi̇' |
['i', 'yi'] |
split in two |
'IYI'.lower() |
'iyi' |
['iyi'] |
wrongly merged |
The repair is to delete the mark from the text you were given, not to lowercase it differently. Why fix the code anyway? The reader copying it will not have that corpus.
Between CountVectorizer and TfidfVectorizer three things change at once: IDF weighting, sublinear term frequency (log-TF) and L2 row normalisation. Same C = 1.0:
unigram word features, min_df=2 |
accuracy | macro-F1 | F1(neg) |
|---|---|---|---|
| raw counts (= “bag of words”) | 92.14 | 73.51 | 51.30 |
| counts + L2 | 88.07 | 69.57 | 45.85 |
| log-TF + L2 | 88.37 | 70.00 | 46.53 |
| counts + idf + L2 | 90.20 | 72.16 | 49.74 |
| log-TF + idf, no norm | 92.67 | 71.83 | 47.61 |
| log-TF + idf + L2 (= “tf-idf”) | 90.07 | 71.95 | 49.41 |
Adding L2 row normalisation to raw counts takes macro-F1 from 73.51 down to 69.57; IDF, added on top of it, brings the score up to 72.16. “TF-IDF fell behind here because of IDF” is not what the experiment shows.
C chosen on validation, the ordering flipsC chosen on a validation split carved out of train (32,000 fit / 8,000 val), then refit on all 40,000 documents:
The “raw counts ahead of TF-IDF” result did not survive an equal tuning budget: it is not a fixed property of the representations, it depended on a shared, untuned C.
iyi değil (“not good”) and güzel değil (“not nice”) as separate features: negation built with a separate wordbeğendim (“I liked it”) and beğenmedim (“I did not like it”) are separate features at the unigram level tooThis chapter did not measure how much of the 4.26 points comes from which kind of negation; saying that takes a targeted error analysis or an ablation.
| towards POSITIVE | towards NEGATIVE | ||
|---|---|---|---|
gayet (quite) |
+8.04 | etmiyorum (I do not [recommend]) |
−8.16 |
ederim ([I] do) |
+6.94 | değil (not) |
−6.95 |
mükemmel (perfect) |
+6.13 | kötü (bad) |
−6.63 |
teşekkürler (thanks) |
+6.09 | etmem (I would not) |
−6.35 |
harika (wonderful) |
+5.93 | beğenmedim (I did not like it) |
−6.21 |
memnunum (I am pleased) |
+5.84 | iade (return / refund) |
−5.85 |
güzel (nice) |
+5.48 | fakat (however) |
−5.52 |
Global associations learned on this training distribution: not universal sentiment scores, and not a complete explanation of one particular prediction.
word2vec, fastText and doc2vec, trained from scratch on the text of the 40,000 training reviews, without using the labels; no pretrained vectors and no test reviews went into that training.
| model | training settings | from there to a document vector |
|---|---|---|
| word2vec | vector_size=100, window=5, min_count=2, sg=1, negative=5, epochs=10 |
the mean of the vectors of its tokens that are in the vocabulary; none at all → the zero vector |
| fastText | the same, with min_n=3, max_n=6, bucket=500_000 |
every token goes into the mean: a vector is built for out-of-vocabulary forms too, from subword vectors |
| doc2vec | vector_size=100, min_count=2, epochs=20, dm=0 |
training documents: learned directly; test documents: infer_vector at its default settings |
All three matrices go to the same logistic regression as in round 1.
### both rounds, sorted by macro-F1
tf-idf 1-2 gram 92.57 76.21 56.47 101,659
bag of words 92.14 73.51 51.30 30,195
tf-idf 90.07 71.95 49.41 30,195
tf-idf char_wb 3-5 88.63 70.39 47.14 94,939
averaged word2vec 79.92 61.39 34.64 100
averaged fastText 79.64 61.36 34.79 100
doc2vec (PV-DBOW) 71.96 55.13 27.66 100
majority baseline 93.68 48.37 0.00 —Paired bootstrap, 10,000 test reviews: macro-F1 difference and 95% interval.
tf-idf 1-2 gram - bag of words +2.69 [ +1.60, +3.82]
bag of words - tf-idf +1.56 [ +0.53, +2.55]
tf-idf - tf-idf char_wb 3-5 +1.57 [ +0.59, +2.54]
tf-idf char_wb 3-5 - averaged word2vec +9.01 [ +7.97, +10.01]
averaged word2vec - averaged fastText +0.08 [ -0.60, +0.77]
averaged fastText - doc2vec (PV-DBOW) +5.85 [ +5.02, +6.69]
tf-idf 1-2 gram - averaged word2vec +14.83 [+13.65, +16.01]| bigram TF-IDF | averaged word2vec | |
|---|---|---|
| macro-F1 | 76.21 | 61.39 |
| F1 on the negative class | 56.47 | 34.64 |
| recall on the negative class | 76.3% | 84.2% |
| precision on the negative class | 44.8% | 21.8% |
These are aggregate rates over the test set. That pattern of false alarms may have as much to do with class_weight="balanced", the decision threshold and the choice of C as with how well the representation separates the classes.
C was tuned for no row at all; every row comes from one split and one runWhat does not depend on corpus size is the averaging defect: that failure is in the pooling step, not in the vectors. That is the transferable result of this chapter; the ranking is the local one.
| word TF-IDF matrix | dense document matrix | |
|---|---|---|
| columns | 30,195 | 100 |
| non-zeros per row | 19.8 on average | 100 |
| measured memory of the training matrix | 9.7 MB | 16.0 MB |
61.39 macro-F1 from 100 numbers per document, against 76.21 from 101,659 columns. Measure the costs in your target use separately.
### nearest neighbours for unseen word forms
kargolanmamış ("not having been shipped")
word2vec KeyError
fastText kargolanmış, kargolandı, kargolanma, kargolanıp, kargolu
tavsiyelerim ("my recommendations")
word2vec KeyError
fastText tavsiyeleri, tavsiyeler, tavsiyeederim, tavsiyedir
hızlıcana (colloquial "nice and fast")
word2vec KeyError
fastText hızlıca, hızlıda, hızlısı, hızlıydı, hızlıThe first neighbour of kargolanmamış (“not yet shipped”) is kargolanmış (“shipped”), which carries no negative suffix: similarity of form can overshadow the negation.
| query | word2vec | fastText |
|---|---|---|
kargo (“shipping”) |
teslimat, gönderim, gönderi, kargoda, kargoyla, kargolama — different words, same topic | kargocu, kargolu, kargosu, kargonuz, kargom, kargomun |
kalitesiz (“low quality”) |
dandik, kullanışsız, dayanıksız, uyduruk, kırılacak — shoddy, unusable, flimsy, fake, about-to-break | kalitesizmiş, kalitesizliği, kalitel, kalitesi, kalitesindeki — all forms of the same stem |
fastText’s list contains kalitesi (“its quality”) — not a synonym: a noun form without the negation that -siz carries. The measurement counts orthographic overlap only; it does not say how appropriate the neighbours are in meaning.
| word2vec | fastText | |
|---|---|---|
| distinct test forms the model has a vector for | 53.6% | 100% |
| macro-F1 in this run | 61.39 | 61.36 |
Producing a vector does not guarantee the right meaning. Measure intrinsic properties, coverage and task performance separately.
Both results hold only for the two setups examined and this test data. The interesting question is what the 272 look like.
| review | gold | who is right |
|---|---|---|
"en guvenli alısveriş sitesi" |
positive | fastText — diacritics partly dropped |
"çok hizli ve temi̇z tesli̇mat" |
positive | fastText — broken diacritics mid-word |
"çok sessiz çalışıyor makina ." |
positive | fastText — “makina”, colloquial for “makine” |
"sıkıcı edebi yonu cok zayif" |
negative | TF-IDF — fastText says positive |
"koku ne çok kötü ne çok iyi" |
negative | TF-IDF — “neither… nor…” |
"cok guzel! kedi bayildi buna" |
positive | TF-IDF — fastText says negative |
### cosine between a review and its negation
averaged tf-idf
fastText
ürünü çok beğendim / ürünü hiç beğenmedim 0.776 0.193
kesinlikle tavsiye ederim / … tavsiye etmem 0.919 0.474
kargo çok hızlı geldi / kargo çok yavaş geldi 0.943 0.572
fiyatına göre gayet kaliteli / … hiç kaliteli değil 0.892 0.690
bu üründen memnun kaldım / … memnun kalmadım 0.962 0.582Averaging can reduce the effect of the one word that changed, through the words the two sentences share — but the whole gap cannot be put down to averaging alone.
\[\mathbf{d}_+ - \mathbf{d}_- = \frac{1}{5}\left(\mathbf{v}_{\text{liked}} - \mathbf{v}_{\text{disliked}}\right)\]
beğendim and beğenmedim is 0.74 in fastText and 0.53 in word2vecAveraging can weaken some signals; these examples do not measure its share of the total 14.82-point gap.
### macro-F1 by labelled training size (same test set of 10,000)
n negatives tf-idf tf-idf char word2vec fastText
100 6 48.37 48.37 53.51 53.87
250 16 48.53 48.84 61.59 63.10
500 32 49.28 50.60 61.02 61.16
1,000 63 55.40 58.55 59.52 59.93
2,500 158 66.61 68.36 60.91 61.19
5,000 316 71.16 70.47 61.52 60.80
10,000 632 72.55 71.29 61.80 61.56
20,000 1,264 72.42 71.26 60.94 60.96
40,000 2,529 71.95 70.39 61.39 61.36| n | tf-idf | tf-idf 1-2 gram | fastText | tf-idf > fastText |
|---|---|---|---|---|
| 250 | 48.98 | 48.70 | 60.96 | 0 / 10 |
| 1,000 | 56.57 | 51.68 | 61.96 | 0 / 10 |
| 1,250 | 59.59 | 53.43 | 62.00 | 2 / 10 |
| 1,500 | 61.87 | 55.63 | 61.90 | 4 / 10 |
| 1,750 | 63.95 | 57.81 | 61.64 | 10 / 10 |
| 2,500 | 67.52 | 63.42 | 61.55 | 10 / 10 |
| 5,000 | 70.97 | 71.49 | 61.18 | 10 / 10 |
In this setup the ordering changes between 1,250 and 1,750 labels. Read any ordering that rests on a single subsample together with that spread.
What transfers is not the threshold but the shape: with few labels a representation learned from unlabelled text can have a head start; as labels accumulate the sparse linear model closes it.
Testing this explanation means looking at the low-label coefficients and errors, and at stronger regularisation options; this chapter did not do that.
What generalises is the question itself: on this task, am I limited by labels, by the representation, or by the settings?
On one CPU core, 40,000 training documents; single run, not repeated.
training matrix is the memory of the training matrix only: it does not cover the model, the vocabulary or the embedding table, or peak RAMNo measurement here supports making doc2vec the default on this short, noisy review task. Look at the absolute number too: 0.17 milliseconds per document is still small for most services.
| setup | macro-F1 |
|---|---|
| averaged fastText alone | 61.36 |
| char TF-IDF alone | 70.39 |
| char TF-IDF ⊕ averaged fastText, plain concatenation (95,039 dimensions) | 71.65 |
char TF-IDF alone, with the same tuning budget (C = 30) |
73.46 |
hybrid with the dense block L2-normalised and scaled (\(\alpha = 2\), C = 10) |
74.69 |
| word bigram TF-IDF, untuned | 76.21 |
Reporting the hybrid as a success by comparing it only against its own parts would be exactly the kind of selective baseline this chapter is arguing against.
| measured in this experiment | choose it when | |
|---|---|---|
| Bag of words | At a fixed C = 1.0 it scored above word TF-IDF (73.51 against 71.95). Does not normalise for document length by itself |
One of the first baselines for short-document classification; choose its settings on a validation set |
| TF-IDF (words) | With C tuned on validation it scored 74.71 and moved ahead of raw counts; fell behind under a shared, untuned C |
A general-purpose first sparse baseline; choose the vectoriser and C inside the train/validation boundary |
| TF-IDF (word unigrams + bigrams) | Highest macro-F1 in this chapter: 76.21. With few labels it stayed below unigram TF-IDF | A few thousand labels and a short-text task where local patterns may matter — sentiment, intent, triage |
| TF-IDF (char n-grams) | 3.2 points ahead of word TF-IDF at 1,000 labels; 144.8 MB training matrix; lower macro-F1 at full data (70.39) | Spelling is variable, and the memory and score you measure are acceptable |
| measured in this experiment | choose it when | |
|---|---|---|
| Averaged word2vec | 61.39 macro-F1 from a 100-dimensional document vector. Ahead of word TF-IDF in all ten label samples up to 1,000 labels. 46.4% of the distinct test forms are not in the vocabulary | Labels are scarce, or you need dense-vector search; as a candidate to test on the target data |
| Averaged fastText | Built a vector for every test form. Highest score at 250 labels on the single-subsample curve (63.10). Its macro-F1 could not be distinguished from word2vec’s | Spelling and form variety are high; with task performance and memory measured |
| doc2vec (PV-DBOW) | Lowest score in this run, 55.13. Inferring a vector for a new document needs extra optimisation (16× averaging) | A separate experiment shows that learning the document vector directly beats averaging on your data |
The choice of method should follow the task and the data budget.
A list of questions, not a list of thresholds. The numbers this chapter observed are useful as starting expectations at step 5, not as rules at step 1.
Document-level sentiment can tolerate this loss better than aspect-level tasks; for “which component is the customer complaining about” it directly blocks the task.
pump from failed: covering that link with one contiguous n-gram takes a seven-token sequenceFor longer relations the representation has to be a function of the sequence: sequence-aware encoders, where Part II starts.
Your client has 400 labelled support tickets in Turkish and 2 million unlabelled ones. Which representation?
A colleague reports 93% accuracy and wants to ship. What do you ask for?
fastText raised form coverage to 100%. Why did the score not go up?
Would 300-dimensional vectors, or pretrained ones, have changed the verdict?
Three of the chapter’s six questions. Almost every answer rests on something this chapter measured.
First name the axis that decides the task: the number of labels, the variety of word forms, the need for order, or cost. Then give the relevant measurement, and say which new data or experiment would change the conclusion.
Cclass_weight="balanced" and rerun every modelThe chapter: A Representation Bake-Off. The lab follows the chapter notebook, en/chapters/04-bake-off/04-bake-off.ipynb in the notebooks repository.
Uygulamalı Doğal Dil İşleme · Applied NLP