Start on Oct 7, 2026 | Due on Oct 23, 2026
Out on Oct 7, 2026
Posted on Coursys.
Please submit your conceptual question to Gradescope and your programming question to Coursys.
Use the public starter repository below and your group’s existing SFU GitHub repository. Replace USER and GROUP with the repository owner’s SFU username and your group name. Run these commands from a parent directory that will contain both repositories.
For a fresh starter checkout:
git clone https://github.com/zhiyunrichardpeng/nlp-class-hw.git
To update an existing starter checkout instead, first save any local work, then run from its parent directory:
git -C nlp-class-hw pull --ff-only origin master
Clone your group repository if necessary:
git clone git@github.sfu.ca:USER/nlpclass-1261-g-GROUP.git
Copy the contents of nlp-class-hw/bertchunker/ into nlpclass-1261-g-GROUP/hw2/. The hw2 directory should directly contain answer/, data/, requirements.txt, zipout.py, zipsrc.py, and check.py; do not add another nested bertchunker directory. Preserve any work already in your group’s hw2 directory. Run all remaining commands with hw2 as the current directory.
Use Python 3.10 or later. On Linux/macOS, create and activate the environment:
python3.10 -m venv venv
source venv/bin/activate
python -m pip install -r requirements.txt
If Python 3.10 is not your executable’s name, substitute your installed Python 3.10-or-later executable. On later sessions, activate the existing environment again.
On Windows PowerShell, you can use the environment directly:
py -3 -m venv venv
.\venv\Scripts\python.exe --version
.\venv\Scripts\python.exe -m pip install -r requirements.txt
Confirm the version is at least 3.10. For each later python3 command, use .\venv\Scripts\python.exe on Windows. In addition, pass the Windows interpreter to zipout.py:
.\venv\Scripts\python.exe zipout.py -x .\venv\Scripts\python.exe
zipout.py otherwise looks for the Unix path venv/bin/python3. Keep the environment and model checkpoints out of your Git commits.
The syntax of a natural language, similar to the syntax of a programming language involves the arrangement of tokens into meaningful groups. Phrasal chunking is the task of finding non-recursive syntactic groups of words. For example, the sentence:
He reckons the current account deficit will narrow to only # 1.8 billion in September .
can be divided into phrasal chunks as follows1:
[NP He] [VP reckons] [NP the current account deficit] [VP will narrow] [PP to] [NP only # 1.8 billion] [PP in] [NP September] .
The training data has three columns separated by whitespace. The supplied development and test input files have two columns (word and part-of-speech tag); the development chunk labels are provided separately in data/reference/dev.out.
Each word has been put on a separate line and there is an empty
line after each sentence.
In the training data, the first column contains the current word, the second column is the part-of-speech tag for that word, and the third column is the chunk tag.
Here is an example of the file format:
He PRP B-NP
reckons VBZ B-VP
the DT B-NP
current JJ I-NP
account NN I-NP
deficit NN I-NP
will MD B-VP
narrow VB I-VP
to TO B-PP
only RB B-NP
# # I-NP
1.8 CD I-NP
billion CD I-NP
in IN B-PP
September NNP B-NP
. . O
The chunk tags contain the name of the chunk type, for example I-NP
for noun phrase words and I-VP for verb phrase words. Most chunk
types have two types of chunk tags, B-CHUNK for the first word of
the chunk and I-CHUNK for each other word in the chunk. The full set of tags
for this task is in the file data/tagset.txt.
The sequence of labels, B-NP, …, I-NP represents a single
phrasal chunk. For instance, the following sequence of labels:
the DT B-NP
current JJ I-NP
account NN I-NP
deficit NN I-NP
gives us the NP phrase:
[NP the current account deficit]
The O chunk tag is used for tokens which are not part of any chunk.
The data set comes from the Conference on Natural Language Learning: CoNLL 2000 shared task2.
There is a helpful program count_sentences.py which allows you
to count how many sentences are in a CoNLL formatted file.
This homework is not just about phrasal chunking but robust phrasal chunking. The input data in dev and test files have been infected with noise so the input to your chunker will look like this:
Rqckwell NNP
, ,
based VBN
in IN
El NNP
Segundo NNP
, ,
Calief. NNP
, ,
is VBZ
an DT
aerospace NN
, ,
electronics NNS
, ,
automotive JJ
and CC
graphics NNS
concern VBP
. .
As you see the words have been infected with noise so
that it contains several spelling mistakes, e.g. Rockwell
is now Rqckwell. The training data is clean, so these spelling
variations may be unfamiliar to the model. The subword tokenizer
can split a noisy word into pieces instead of treating every noisy
word as an unknown token.
The input files do not have the output chunk labels
which appear in data/reference/dev.out for input data/input/dev.txt.
The data files provided are:
data/train.txt.gz – the training data used to train the answer/default.py modeldata/input – input files dev.txt and test.txt infected with noisedata/reference/dev.out – the reference output for the dev.txt input fileThe default solution is provided in answer/default.py. From hw2, copy it
as the starting point for your solution:
cp answer/default.py answer/bertchunker.py
cp answer/default.ipynb answer/bertchunker.ipynb
Keep any existing solution and notebook before copying. If you have a
checkpoint from an earlier starter, regenerate it with the corrected
starter before running zipout.py:
python3 answer/default.py --force > output.txt
This trains a fresh baseline and replaces data/chunker.pt and the matching
epoch checkpoints. Back up any previous experiment checkpoints first.
On Windows, train without shell redirection, then collect and check the
baseline predictions with the supplied scripts:
.\venv\Scripts\python.exe answer/default.py --force
.\venv\Scripts\python.exe zipout.py -r default.py -x .\venv\Scripts\python.exe -o baseline-output -z baseline-output
.\venv\Scripts\python.exe check.py -z baseline-output.zip
Check baseline-output/dev.ret and baseline-output/test.ret for 0.
This avoids the different output encoding used by older PowerShell versions
for > redirection. The output.txt / conlleval.py examples below are for
Linux/macOS; use the archive-based commands above on Windows.
After changing your own model or training settings, use
python3 answer/bertchunker.py --force > output.txt to regenerate its checkpoint.
On Windows, use .\venv\Scripts\python.exe answer/bertchunker.py --force
without redirection, followed by the Windows zipout.py command above under
Getting Started. Keep your baseline and method results separately for comparison.
The short form of --force is -f.
Then generate and check the outputs:
python3 zipout.py
python3 check.py
The default solution will look for the file chunker.pt
in the data directory. If it does not find this file it
will start training on data/train.txt.gz. Training time depends on the device and configuration; allow substantially more time on a CPU. The program also downloads the base model and tokenizer when they are not cached.
You can also download the chunker.pt model
file
that was trained using default.py.
Please do not commit the file into your git repository as it is moderately large and you can go over your disk quota.
Once you have trained the corrected baseline at data/chunker.pt, you can run:
python3 answer/default.py > output.txt
And then you can check the score on the dev output file called output.txt by running:
python3 conlleval.py -o output.txt
which produces the following detailed evaluation:
processed 23663 tokens with 11896 phrases; found: 11847 phrases; correct: 10764.
accuracy: 94.01%; (non-O)
accuracy: 94.37%; precision: 90.86%; recall: 90.48%; FB1: 90.67
ADJP: precision: 79.29%; recall: 69.47%; FB1: 74.06 198
ADVP: precision: 74.25%; recall: 74.62%; FB1: 74.44 400
CONJP: precision: 66.67%; recall: 85.71%; FB1: 75.00 9
INTJ: precision: 100.00%; recall: 100.00%; FB1: 100.00 1
NP: precision: 90.22%; recall: 91.57%; FB1: 90.89 6330
PP: precision: 96.80%; recall: 94.31%; FB1: 95.54 2378
PRT: precision: 80.56%; recall: 64.44%; FB1: 71.60 36
SBAR: precision: 92.27%; recall: 75.53%; FB1: 83.06 194
VP: precision: 90.48%; recall: 90.36%; FB1: 90.42 2301
(90.85844517599392, 90.48419636852724, 90.67093459124794)
For this homework we will be scoring your solution based on the FB1 score which is described in detail in the Check your performance section below. However the FB1 score is not the only focus. You can focus on efficiency, model size, experimental comparison with other approaches and many other choices.
Make sure that the command line options are kept as they are in
answer/default.py. You can add to them but you must not delete any
command line options that exist in answer/default.py.
Submitting the default solution without modification will get you zero marks.
The model used in answer/default.py is a BERT-based Transformer
model (distilbert-base-uncased) that is fine-tuned to predict the phrase
chunking tags for each (sub-word) token. It is trained on the data provided
in data/train.txt.gz which has the ground truth phrase tags
for each token and these sentences are used to fine-tune the
BERT encoder along with the classification head.
Important Note on BERT: Fine-tuning is expected—do NOT freeze BERT. BERT’s parameters must remain in the optimizer and be fine-tuned along with the classification head. When BERT is frozen, the model cannot sufficiently adapt contextual embeddings to the chunking task, causing performance to drop significantly.
After training the baseline at data/chunker.pt, inspect its model structure from the hw2 directory:
python3 answer/model.py
This prints out the model:
TransformerModel(
(encoder): DistilBertModel(
(embeddings): Embeddings(
(word_embeddings): Embedding(30522, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(transformer): Transformer(
(layer): ModuleList(
(0-5): 6 x TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
(activation): GELUActivation()
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
)
)
)
(classification_head): Linear(in_features=768, out_features=22, bias=True)
)
Optimizing the above parameters to find the minimum loss on the
training data by gradient descent is done automatically using PyTorch
API calls in answer/default.py. Both the encoder and classification head
are updated during training.
For this homework we will enforce that the base BERT model should not
be changed: use distilbert-base-uncased as your base BERT model.
However, you must fine-tune its parameters—do not freeze the encoder.
You can change the fine-tuning architecture, hyperparameters (including
learning rate, number of epochs, and batch size), and added layers/components
as you wish.
You may tune training hyperparameters, including the learning rate,
number of epochs, and batch size. You may also tune hyperparameters of
your added components. Record your settings and comparison results in
answer/bertchunker.ipynb.
You will need to use some Pytorch API calls to solve this homework.
We do not expect you to already know Pytorch in great detail.
The following links will help you get started but you can learn
a lot of the Pytorch basics by understanding default.py and
the process of solving this homework.
Some useful links if you feel lost at the beginning:
Read the source code in answer/default.py in detail.
Your task is to improve robust phrasal chunking by fine-tuning the
distilbert-base-uncased model for the phrasal chunker. You may tune training
hyperparameters and improve the model architecture and training strategies,
as described below.
The score is explained in detail in the Check your performance
section below. With substantial computational resources and using
large pre-trained models (which are beyond the scope of this homework)
the state of the art accuracy on this
dataset has
reached an F1-score above 97 percent (typically by using more
information than just the training data available for this
task).
However, the numbers from that leaderboard do not apply to the dev and test data in this homework. The dev and test data used here are much more challenging than the standard CoNLL 2000 chunking task because several typos have been introduced into the data (as explained above).
Here are some specific things you can try to improve the accuracy of the fine-tuned model:
You only need to try one or two of these ideas to improve your model for this homework. In your notebook, compare your approaches (e.g., baseline vs. MLP / CRF / other methods) using the fine-tuned base encoder. Report the dev scores you actually obtain.
A very simple idea for dealing with the misspellings in the dev and test data is to realize that the training data is not similarly noisy. Augmenting the training data with additional noisy examples can help the model handle misspellings at inference time.
For more advanced approach, look into adversarial training as explained in the following paper:
Combating Adversarial Misspellings with Robust Word Recognition. Danish Pruthi, Bhuwan Dhingra, Zachary C. Lipton. ACL 2019.
The classification head in default.py is a single linear layer. You could experiment with more expressive feed-forward networks like the following model.
FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
(classification_head): Linear(in_features=768, out_features=22, bias=True)
)
You can experiment with lin1 having more parameters than the input or fewer parameters.
A linear-chain Conditional Random Field (CRF) scores a complete label sequence using emission scores from the classification head and learned transition scores between labels.
For a sentence of length \(n\), let \(E_{i,k}\) be the emission score for tag \(k\) at token \(i\) and \(A_{j,k}\) a transition score from tag \(j\) to tag \(k\). With separate start and end scores, define
$$s(\mathbf{x},\mathbf{y}) = A_{\mathrm{START},y_1}+\sum_{i=1}^{n}E_{i,y_i}+\sum_{i=2}^{n}A_{y_{i-1},y_i}+A_{y_n,\mathrm{END}}.$$
The conditional log probability is
$$\log p(\mathbf{y}\mid\mathbf{x})=s(\mathbf{x},\mathbf{y})-\log\sum_{\mathbf{y}'\in\mathcal{Y}(\mathbf{x})}\exp s(\mathbf{x},\mathbf{y}').$$
Here, \(\mathcal{Y}(\mathbf{x})\) is the set of candidate label sequences
for the input. The normalization ranges over label sequences, not input
tokens. Train by minimizing the negative log probability of the gold
sequence, using the forward algorithm to compute the normalization, and
decode the highest-scoring sequence with Viterbi. Independent per-token
argmax is not CRF decoding. Transition and emission values are scores,
not probabilities to add after separate softmax operations.
In the starter, tag_space has shape [batch, length, number_of_tags];
the tag dimension is size(-1) (or size(2)), not size(3). Keep padding
out of the sequence score and loss with a valid-token mask. Use explicit
start/end states rather than connecting the first token to the last.
Preserve the mapping between subwords and output words. BIO constraints,
if imposed, should allow both B-X followed by I-X and I-X followed
by I-X within a span.
A reference for neural sequence labeling with a CRF layer is:
Neural Architectures for Named Entity Recognition. Guillaume Lample, Miguel Ballesteros, Sandeep Subramanian, Kazuya Kawakami, Chris Dyer. NAACL 2016.
Based on the original CRF paper (which you don’t really need to dive into but linked here for completeness):
Conditional Random Fields: Probabilistic Models for Segmenting and Labeling Sequence Data. John Lafferty, Andrew McCallum, Fernando Pereira. ICML 2001.
Implementing a CRF layer is a useful PyTorch exercise. Compare its measured performance and errors against the baseline and MLP models in your notebook. You may also investigate a head with self-attention as an additional method.
You must create the following files:
answer/bertchunker.py – your solution, starting from answer/default.py as explained above.answer/bertchunker.ipynb – your notebook report, including documentation and analysis. No separate report.pdf is required for HW2-P.To create the output.zip file for upload to Coursys do:
python3 zipout.py
For more options:
python3 zipout.py -h
To check your performance on the dev set:
python3 check.py
The output score is the $F_{\beta=1}$ score or FB1 score which is the harmonic mean of the precision and recall computed over all the output phrasal chunks.
python3 check.py -h
In particular use the log file to check your output evaluation:
python3 check.py -l log
The performance on data/input/test.txt will not be shown. We will
evaluate your output on the test input after the submission deadline.
Prepare a clear summary of what you did in answer/bertchunker.ipynb, which serves as your report. Organize the documentation and analysis into clear sections, with full sentences. Use figures, graphs, and tables to compare experimental results.
The documentation and analysis in your python notebook should include the following:
Compare the performance of the default baseline solution vs using an MLP head, a CRF layer, or any other methods you attempted (all using the fine-tuned distilbert-base-uncased base model). Document differences in training settings, and discuss which approach worked best and what errors were made by the different approaches.
Once you are done with your homework submit all the relevant materials to Coursys for evaluation.
Once you have a working solution in answer/bertchunker.py create
the output.zip for upload to Coursys using:
python3 zipout.py
Before uploading, check that output/dev.ret and output/test.ret both contain 0. Inspect output/dev.err and output/test.err if a run failed. The presence of output.zip alone does not establish that both runs succeeded.
To create the source.zip file for upload to Coursys do:
python3 zipsrc.py
You must have the following files or zipsrc.py will complain about it:
answer/bertchunker.py – your solution, starting from answer/default.py as explained above.answer/bertchunker.ipynb – your notebook report, including documentation and analysis.In addition, each group member should write a short description of their contribution in answer/README.username, replacing username with their own SFU ID. zipsrc.py checks for the Python and notebook filenames but does not check that every member has a contribution file; check these yourself.
source.zip contains the contents of answer/ at its archive root. Include any helper Python files your solution imports and an answer/requirements.txt if your dependencies differ from the starter. Inspect the archive before uploading to ensure these files and all contribution statements are included.
Go to Programming Homework 2 on Coursys and do a group submission:
output.zip and source.zip to Coursysanswer/bertchunker.ipynb (which serves as your report).answer/README.username.The grading is split up into the following components:
answer/README.username.Your F-score should be equal to or greater than the score listed for the corresponding marks.
| Score(dev) | Score(test) | Marks | Grade |
| Nan | Nan | 0 | F |
| 90.5 | 82 | 55 | D |
| 91 | 83 | 60 | C- |
| 91.5 | 84 | 65 | C |
| 92 | 85 | 70 | C+ |
| 92.5 | 86 | 75 | B- |
| 93 | 87 | 80 | B |
| 93.5 | 88 | 85 | B+ |
| 94 | 90 | 90 | A- |
| 94.5 | 92 | 95 | A |
| 96 | 95 | 100 | A+ |
The score will be normalized to the marks on Coursys for the dev and test scores.
Caveat: If you have a linguistic background, you might find the verb phrases VP and prepositional phrases PP are different from what you might be used to. In this task, the VP is a verb and verb modifiers like auxiliaries (were) or modals (might), and the PP simply contains the preposition. This difference is because of the fact that the chunks are non-recursive (cannot contain other phrases) – we need trees for full syntax. ↩