<?xml version='1.0' encoding='UTF-8'?>
<rss version='2.0'>
<channel>
<title>Adrien Foucart's research blog</title>
<link>https://research.adfoucart.be</link>
<description>Research and thoughts and artificial intelligence and biomedical imaging.</description>
<language>en-uk</language><item>
    <title>Wrapping up and looking back</title>
    <link>https://research.adfoucart.be/wrapping-up</link>
    <description><p>I have been very lucky through my academic career, but in the end not
lucky enough. September 26th will be my last day as a post-doc
researcher, and as I am in the process of wrapping up my research and
cleaning out my desk, I would like to indulge in a tiny bit of
self-absorbed rambling (it’s my blog, I’m allowed to do that). To make
it a little bit easier on anyone who actually wants to read this, here
is a table of contents (please don’t use an AI summary: read it or
don’t!):</p>
<ul>
<li><a href="#a-quick-summary-of-my-academic-career">A quick summary of
my academic career</a></li>
<li><a href="#lessons-from-academia">Lessons from academia</a>
<ul>
<li><a href="#so-much-waste-from-precarious-jobs">So much waste from
precarious jobs</a></li>
<li><a href="#closed-source-science-is-not-science">Closed-source
science is not science</a></li>
<li><a href="#local-and-organic-science">Local and organic
science</a></li>
<li><a href="#generative-ai-is-making-science-worse">Generative AI is
making science worse</a></li>
<li><a href="#generative-ai-is-making-teaching-worse">Generative AI is
making teaching worse</a></li>
</ul></li>
<li><a href="#whats-next-for-me">What’s next for me?</a></li>
</ul>
<h1 id="a-quick-summary-of-my-academic-career">A quick summary of my
academic career</h1>
<p>I joined the <a href="https://lisa.polytech.ulb.be">LISA</a> at the
Université Libre de Bruxelles in 2011, as a researcher on an
academia/industry project. I worked for two years on vehicle detection
and classification from traffic cameras, and failed to secure funding to
start a PhD twice before giving up and moving to the private sector for
two years. In 2015, a teaching assistant position opened up and I came
back, this time working on digital pathology image analysis. I defended
my PhD in 2022, and started, still in the same lab, on a postdoc,
working on multimodal image registration in preclinical imaging.
Finally, in early 2025, I applied for a tenured position on multimodal
AI in medicine, and came in second in the selection process (which is a
nice way of saying that I wasn’t selected). As this was the
opportunity for me to finally get a permanent position, I quickly
decided not to try for another round of postdoc funding. It’s time to
go, more on that <a href="#whats-next-for-me">at the end of this
post</a>.</p>
<p>So what did I learn from the world of academia?</p>
<h1 id="lessons-from-academia">Lessons from academia</h1>
<h2 id="so-much-waste-from-precarious-jobs">So much waste from
precarious jobs</h2>
<p>Most of the people doing most of the day-to-day scientific work in
academia are on short term, precarious contracts where funding can
quickly vanish. This means that most of the people who should
be focused on their scientific work are in reality focusing a large part
of their time and energy on securing the next round of funding.</p>
<p>This is not a very efficient use of our limited resources, to say the
least, and it’s not getting better.</p>
<p>The precarity of academic jobs also means that researchers
need to worry a lot about publishing, as often as
possible, because that’s the metric that we’re apparently stuck with.
Not publishing enough means fewer chances of converting these short-term
jobs into a tenured position.</p>
<p>Publishing scientific output is great, and a necessary part of
science, but when it becomes the goal rather than a means of
communicating about our results, it becomes a very problematic
incentive. There is a huge crisis of fake science going on right now
(see <a href="https://retractionwatch.com/">Retraction Watch</a>, or
Elisabeth Bik’s <a href="https://scienceintegritydigest.com/">Science
Integrity Digest</a>, or anything posted by <a
href="https://www.linkedin.com/in/guillaumecabanac/recent-activity/all/">Guillaume
Cabanac</a> for examples…), which started even before generative AI
became ubiquitous. A big factor in how much bullshit gets through the
peer review process is just the insane amount of articles that are being
pushed through the process. Peer reviewers take less time per articles,
and are themselves sometimes incentivized to go through articles quickly
in order to gain publication credits.</p>
<p>If I had to start over and choose my publication strategy right now,
I would no longer look at impact factors, and I would stay away from the
Big Publishers (Elsevier, Springer/Nature…). Instead, I would look at
true <a href="https://www.diamondopen.com/journals-list/">Diamond
Access</a> journals, making sure to always also make supporting code and
data available.</p>
<p>Which leads me to…</p>
<h2 id="closed-source-science-is-not-science">Closed-source science is
not science</h2>
<p>Most of the research in my domain is supported by software. Most of
our experimental work is, generally, applying some software to some
data. That is, in theory, great for replicability… as long as
the code and data are made available to the rest of the research
community.</p>
<p>I have become more radicalized on this as time goes on, and as I’ve
seen how often published results are found to be wrong when
it’s possible to check them. In fact, a not insignificant part of <a
href="https://research.adfoucart.be/publications">my own research</a>
was based off finding other people’s mistakes. The truth is, researchers
(even in AI-related fields) are not necessarily good software
developers. There is a reason why Greg Wilson et al.’s <a
href="https://journals.plos.org/plosbiology/article?id=10.1371/journal.pbio.1001745">Best
practices for scientific computing</a> had to be followed up by <a
href="https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1005510">Good
enough practices for scientific computing</a> a few years later:
everyone’s research depends on some software development, and
most scientists are not developers.</p>
<p>The only way for any scientific result to be useful for the rest of
the community (and not just to add a DOI to a researcher’s belt) is if
the supporting software and data are open source, so that mistakes can
be fixed, and misrepresentations of the software in the text can be
commented on.</p>
<p>It’s of course not always possible to publish all the data
(particularly with medical data), but there should always be enough to
be able to check that the code is doing what it’s supposed to be
doing.</p>
<h2 id="local-and-organic-science">Local and organic science</h2>
<p>The best, most constructive interactions I’ve had with colleagues
over my research (or over their research) has always been at small scale
events. Small, local conferences. Informal meetings in the lab. Talking
over lunch while visiting another lab.</p>
<p>Huge conferences are great on the resume, but for me at least they
just leave me exhausted from too much noise to sift through.</p>
<p>Did you know that <a
href="https://iccv.thecvf.com/Conferences/2025/Changes">ICCV</a> has
recently limited each author “to a maximum of 25 paper submissions”?
This is insane. The idea that a person can significantly contribute to
25 papers at the same conference is absolutely ridiculous. At the SIPAIM
2023 conference, I was first author of <a
href="https://doi.org/10.1109/SIPAIM56729.2023.10373416">one paper</a>
and second author of <a
href="https://doi.org/10.1109/SIPAIM56729.2023.10373514">another</a>,
and I really don’t think I could have contributed anything valuable to
another one at the same time, even though these were small
“work-in-progress” paper for a small (but very good!) conference.</p>
<p>The limited resources of research labs are in my opinion better used
going to or organizing smaller scale events rather than trying to go for
the prestigious conferences.</p>
<h2 id="generative-ai-is-making-science-worse">Generative AI is making
science worse</h2>
<p>I don’t think ChatGPT is a very good program.</p>
<p>I think the GPT models are very good LLMs, and that LLMs, Vision
Transformers, and other large modern architectures are really
interesting and can do great things in many different fields. But
chatbots are one of the worse way of using them.</p>
<p>Unfortunately, many researchers are now using ChatGPT (or Claude,
etc., but mostly ChatGPT) to write their code, their research papers,
and to summarize the existing literature. The problem is that chatbots
are notoriously bad at summarizing things, at writing things that
actually make sense, and at writing code that does what it’s supposed to
do.</p>
<p>It also make people worse at critical thinking, and limit even more
than before the opportunity for lesser known labs and researchers to get
“on the radar” of their community. With ChatGPT, the state-of-the-art
will be incomplete, the experimental results are likely to be
irrelevant, and the write-up will be filled with bullshit and
plagiarism.</p>
<p>So maybe we shouldn’t do that?</p>
<h2 id="generative-ai-is-making-teaching-worse.">Generative AI is making
teaching worse.</h2>
<p>Researchers are not the only one falling for the AI hype. Students
and teachers are, in fact, one of the population most aggressively
pursued by AI salesmen. The accepted knowledge today seems to be that
“AI is here to stay”, that teachers need to adapt to it, and that we
can’t expect students not to use it. Most master theses if not all are
now written “with the help” of a chatbot. And why wouldn’t students use
it? It’s right there, available in institutionally provided software
such as Microsoft’s suite. We are warning students against it from one
side, and providing it to them from the other.</p>
<p>But is it really a bad thing? If generative AI-powered chatbots are
the new reality, then surely students should learn how to use them.
Maybe, but they should also learn when to use them,
when not to use them, and therefore how not to use
them.</p>
<p>Let’s take two hypothetical scenarios for the future of “AI”.</p>
<p>Scenario one is the tech optimist’s scenario, where AI is on a path
of exponential growth and will soon be able to do every intellectual job
that currently requires a human. If AI gets to this point, then what
role could our current students possibly have in this new world? A truly
intelligent AI will not need “prompt engineering”. Prompt engineering is
the art of coaxing a dumb AI into producing somewhat acceptable
results. Truly intelligent AIs are incompatible with our current model
of society: there is no place there for (most) human jobs. Learning “how
to use AI” will not be helpful then. Owning the AI is the key,
and those positions are already filled.</p>
<p>Fortunately, I don’t think scenario one is very likely, at least in
the next decades. GPT-5 was such a flop that Sam Altman quickly had to
restore version 4 and to <a
href="https://www.cnbc.com/2025/08/19/sam-altman-on-gpt-6-people-want-memory.html">start
hyping version 6</a>. It’s been clear since version 3.5 that a plateau
has been reached. We now need to put exponentially more power into it
for very diminishing returns. The “reasoning” models take minutes to
process queries, with results that are still plagued with all the
fundamental problem of LLM-based generative AIs: no actual
understanding of what they are doing.</p>
<p>So scenario two is that, at some point in the not-too-distant future,
the AI hype bubble explodes. That is going to have a nasty
effect on the tech industry. I wouldn’t be surprised if Microsoft
doesn’t survive the crash, as they have gone all-in into generative AI.
It’s also going to mean that, with the departure of investor money, any
remaining AI company will need to push the cost of training and running
those models to the customer. And that’s going to turn AI chatbots
very expensive toys.</p>
<p>Does that mean that generative AI will be going away? Probably not.
But adding friction to the system (such as having to pay for each
interaction) may make people a little bit more willing to spend a couple
of minutes writing a damn email by themselves, reading the material
that’s given to them, and thinking about the exercises that they have to
solve.</p>
<p>Because right now, many students are not learning. They have become
machines that put instructions into a chatbot and copy-paste the
results. Of course, for many exercises, it works well: exercises
designed for students are rarely so original that you can’t find
something very similar online, and most instructors are willing to
accept somewhat flawed answers as long as it seems like the student
learned something. Then, at some point, the student hits a bump on the
road and faces a problem that the chatbot cannot solve. And
now, they can’t think the problem through, because they haven’t
developed any of the necessary skills.</p>
<p>I am very happy to have gone through university before using AI was
an option. I would absolutely have fallen for it at the time. I
certainly used every means available to make passing the exams easier,
even if it meant not interacting with the subject matter fully.
Hopefully we can soon find a way to work around AI, and to convince
students to learn how to use their own brains.</p>
<h1 id="whats-next-for-me">What’s next for me?</h1>
<p>Friday September 26th, 2025, will be my last day as a postdoc
researcher at the Université Libre de Bruxelles.</p>
<p>Monday September 29th, 2025, will be my first day as a teacher at the
Haute École Léonard de Vinci, where I’ll join the teaching team for
their Bachelor in Informatics.</p>
<p>I am very excited about it. I love teaching, and I particularly love
teaching about coding, computer science, and AI. I will have the
opportunity to do all of that there.</p>
<p>That means also that this blog is going to be wrapped up. There won’t
be much research going on. I may post a little bit more about some
ongoing project that I’m finishing, but for the most part my time as a
researcher is done. My time as a “blogger” isn’t, though. I intend to
keep my irregular posting schedule on my personal blog <a
href="https://blog.adfoucart.be">2xRien</a>, and I will be posting (in
French and probably just as irregularly) about my teaching experience on
my brand new blog, <a href="https://clavier.adfoucart.be">clavier
ouvert</a>. The best way to get notified when I post anything, if you’re
interested in following me, is through the RSS feeds available in each
of my blogs. I’m posting less and less of my stuff on social networks. I
think choosing to subscribe to <a href="https://aboutfeeds.com/">RSS
feeds</a> if a much better way of curating your own feed full of
interesting things to read, without Zuckerberg et al. chiming in with
what they think you should read.</p>
<p>This blog will stay right here, and the <a
href="https://research.adfoucart.be/archives">archives</a> will remain
accessible for the time being. I like the idea of keeping a trace of the
things I communicated about through these past five years. But now, on
to new challenges!</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2025-09-15</pubDate>
    </item><item>
    <title>Teaching data analysis to engineering master students</title>
    <link>https://research.adfoucart.be/teaching-data-analysis</link>
    <description><p>A little bit more than a year ago, I was asked to teach the “STAT-H-400” Multivariate data analysis course for a year, as a temporary solution to replace a teacher who had to leave unexpectedly. My hope was to then convert that temporary position into a tenured position with the opening of a chair in Multimodal AI in Medicine this year, but unfortunately I was not selected. My experience as a “university professor” will therefore be short-lived.</p>
<p>I’m still very happy to have had this opportunity, and I’m proud of where I was able to take the course in a year. I’ll keep all the material available at <a href="https://stath400.adfoucart.be/">https://stath400.adfoucart.be/</a>. This is clearly a work in progress, which I probably won’t finish, but who knows? Maybe I’ll come back to it sometimes and find a way to recycle it somehow.</p>
<p>So if you’re interested in <a href="https://stath400.adfoucart.be/notes/choosing-test.html">hypothesis testing</a>, <a href="https://stath400.adfoucart.be/html/module-4.html#principal-component-analysis-pca">PCA</a>, <a href="https://stath400.adfoucart.be/html/module-4.html#dimensionality-reduction">dimensionality reduction</a>, <a href="https://stath400.adfoucart.be/html/module-4.html#clustering">clustering</a> or <a href="https://stath400.adfoucart.be/html/module-5.html">supervised learning</a>, there may be some things of interest for you there.</p>
<p>I also like this <a href="https://stath400.adfoucart.be/notes/mfe-mistakes.html">Top data analysis mistakes in master theses</a> note that I made. The code to produce all experiments and illustrations for the course is also available on <a href="https://gitlab.com/adfoucart/stath400-experiments/">Gitlab</a>.</p>
<p>Teaching is something that I really enjoy (at least at the university level, I don’t know how long I would last in a secondary school setting!), and I hope that whatever comes next for me will include at least some of it. It’s stressful, time-consuming, and frustrating at times, but it’s probably the part of my academic career that I enjoyed the most overall.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2025-08-20</pubDate>
    </item><item>
    <title>Adding context to publications: a small experiment.</title>
    <link>https://research.adfoucart.be/adding-context</link>
    <description><p>I’m making a small experiment with <a href="./publications">my
publications</a> – or at least the version that I host here.</p>
<p>It started like this: I think that <a
href="https://chem-bla-ics.blogspot.com/2009/06/no-pdfs-really-do-suck.html">PDF
kinda suck as a science communication medium</a>, so I wanted to add an
HTML version of my papers. But after converting some of my LaTeX sources
to HTML (thanks to <a href="https://pandoc.org/">pandoc</a>), and using
a little bit of my limited CSS talent, I got something that was OK, but
felt underexploited.</p>
<p>Because now, I have the opportunity to add stuff around the
published text. I don’t think modifying the text itself would be right,
but there are many cases where, due to the length constraints or lack of
knowledge at the time, things that I wrote are not quite as clear as I
would like, or miss some useful informations. Or where I would simply do
things a bit differently know, or have got more recent results that
strengthen or weaken some of the conclusions.</p>
<p>In the current publication model, it’s not very easy to add such
context. You can cite the old publication in your newer ones and comment
from there, of course, but if someone finds the old article first, there
is really nothing that make it clear what may or may not have changed
since publication. But here, on my own website, I can do what I
want !</p>
<h2 id="the-experiment">The experiment</h2>
<p>I found a <a href="https://scripter.co/sidenotes-using-only-css">nice
post</a> explaining how to easily create “sidenotes” using only CSS,
which is great: I don’t want to start messing with Javascript unless
absolutely necessary. I was also inspired by (aka: stole from) the work
of <a href="https://www.arthurperret.fr">Arthur Perret</a>, who has some
great posts on <a
href="https://www.arthurperret.fr/blog/2022-06-22-publication-multiformats-pandoc-make.html">publishing
to multiple formats from text source</a>, and uses similar sidenotes
through his blog and even in the <a
href="https://these.arthurperret.fr/chapitre-1.html">web version of his
PhD dissertation</a> (all in French).</p>
<p>Sidenotes seem like a really good way to add contextual information.
It’s easy to make it absolutely clear that it isn’t part of the original
text itself (for instance, with timestamps on each note) and that it
doesn’t represent the opinion of all original authors (by
signing each note). And it doesn’t distract too much from reading the
text, like footnotes would.</p>
<h2 id="the-results">The results</h2>
<p>I started from the first article I published during my PhD, on <a
href="./pub-html/FOUCART18-Artifact-Annotated.html">artifact
segmentation in digital pathology</a>. I simply re-read it and started
annotating. I quickly found things that I thought were interesting to
add: links to later publications, clarifications, links to relevant
source code, etc.</p>
<p>Moving from one publication to the next, I got more convinced that
this was a good idea. It makes it a lot easier, for instance, to follow
the links between the first paper, the first <a
href="./pub-html/FOUCART19-SNOW.html">SNOW</a> paper from ISBI, and the
<a href="./pub-html/FOUCART20-SNOW.html">follow-up report</a> that
expands on both.</p>
<p>In my early articles, I also didn’t have the habit yet of creating a
snapshot repository with code for replicating the results. So I could
also point to later versions of the code in some of my repositories,
which in some case may make it easier for some to understand exactly
what I was doing.</p>
<p>I’ve now gone through all of the publications where I’m first author,
up to my latest <a
href="https://research.adfoucart.be/pub-html/FOUCART25-ESANN.html">ESANN
2025</a> paper.</p>
<h2 id="conclusions">Conclusions</h2>
<p>I really like this format. Not only because I can correct things that
I think weren’t quite right when we published the paper in the first
place, but also because it allows me, in a way, to personalize a lot
more the way I present that research. My publications are obviously not
just mine, even when I’m the first author. But here, I can add
things in my own words, without pressure, and I think it brings some
life into what is often a fairly dry process.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2025-05-19</pubDate>
    </item><item>
    <title>[Opinion] Science and LLMs don't go together well</title>
    <link>https://research.adfoucart.be/science-llms</link>
    <description><p>Two posts on Retraction Watch yesterday highlight yet again how the
big publishers in the scientific publishing industry are failing at
being gatekeepers of high-quality science, and how many scientists don’t
seem to grasp that they should be writing their papers themselves.</p>
<p>“<a
href="https://retractionwatch.com/2025/02/10/vegetative-electron-microscopy-fingerprint-paper-mill/">As
a nonsense phrase of shady provenance makes the rounds, Elsevier defends
its use</a>” (Frederik Joelving, Retraction Watch).</p>
<p>In this first one, a “fingerprint” of automatically-generated papers
is described. These fingerprints, like the “tortured phrases” identified
by Guillaume Cabanac [<a
href="https://arxiv.org/pdf/2107.06751">arXiv</a>], are basically things
that no scientist will ever write and that no diligent human reviewer or
editor should let through. Cabanac identifies for instance
“profound neural organization” used instead of “deep neural network”, a
clear sign that some automated method just tried to find synonyms in an
effort to mask plagiarism, without understanding the context.</p>
<p>This time, the fingerprint is “vegetative electron microscope” (or
microscopy), which… doesn’t really mean anything? The most likely
explanation found for its appearance (in more than 20 published papers)
is because of an OCR error: a machine tried to convert a two-column 1959
paper to plain text and accidentally merged the two columns, where
“vegetative” and “electron microscopy” appeared next to each other.</p>
<p>But the news here isn’t the fingerprint, but rather the baffling
reaction of Elsevier when asked about a 2024 paper published in one of
their journal [<a
href="https://www.sciencedirect.com/science/article/pii/S0926669023016692?via%3Dihub">Elsevier</a>].
To be clear, “vegetative electron microscope” is something that no one
has ever written and that doesn’t make sense. But a Elsevier spokesman
wrote that “the Editor-in-Chief confirmed that ‘vegetative electron
microscopy’ is a way of conveying ‘electron microscopy of vegetative
structures’ so he was content with the shortened version to be
used.”</p>
<p>This is nonsense. First because, again, no-one uses this
“shortcut”. Second, because the thing studied at that point of the paper
is bacterial cellulose, which is different from plant cellulose
and therefore not even vegetal. The presence of such an obvious mark of
automated paper writing should be an instant retraction, and launch an
investigation into any previous paper published by the authors.</p>
<p>“<a
href="https://retractionwatch.com/2025/02/10/as-springer-nature-journal-clears-ai-papers-one-universitys-retractions-rise-drastically/">As
Springer Nature journal clears AI papers, one university’s retractions
rise drastically</a>” (Avery Orrall, Retraction Watch)</p>
<p>Writing a scientific paper with ChatGPT or other LLMs is a terrible
idea. Using AI assistance to write lowers our critical thinking
abilities, as even Microsoft realises [<a
href="https://www.microsoft.com/en-us/research/publication/the-impact-of-generative-ai-on-critical-thinking-self-reported-reductions-in-cognitive-effort-and-confidence-effects-from-a-survey-of-knowledge-workers/">Microsoft
Research</a>].</p>
<p>Convincing scientists, particularly those under heavy pressure to
publish a lot, not to use these tools is a tall order. The bare
minimum, which the Springer Nature publisher requires, is to
disclose the use of an LLM. More than one hundred papers that failed
this bare minimum requirements have been retracted so far by the
Neurosurgical Review journal. At least in this case the journal
seems to be doing its job, but I find the response from one of the
retracted authors [<a
href="https://retractionwatch.com/wp-content/uploads/2025/02/saveetha_Jeyachandran_email.pdf">Sivakamavalli
Jeyachandran via RetractionWatch</a>], who disagrees with the
retraction, really disheartening.</p>
<p>His argument is:</p>
<ul>
<li>Lack of standard for detecting LLM-written content makes the
retractions arbitrary. That’s true, but “it’s hard to detect fraud”
is a poor argument for “not acting when fraud is detected”.</li>
<li>It was a letter or commentary, not a full research paper, so it
doesn’t really matter. If it matters enough to be published, it
matters enough to be retracted – in my opinion.</li>
<li>Retractions should only be for data fabrication, plagiarism or
breaches of journal policy. Well, this was a breach of
journal policy, so I’m not sure how that’s an argument against
retraction. Also, LLM output is arguably plagiarism by essence, but
that’s another debate…</li>
</ul>
<p>He at some points refers to his undisclosed usage of an LLM as a
“minor technical infraction”. I certainly disagree – and am glad that
Springer seems to disagree as well.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2025-02-11</pubDate>
    </item><item>
    <title>[Preprint] Ranking the scores of algorithms with confidence (ESANN 2025)</title>
    <link>https://research.adfoucart.be/preprint-cirank</link>
    <description><p>In competitions or in original research papers which compare the
results of some algorithms on a given task, the centerpiece is generally
the Big Table of Results. The Big Table of Results is where you put a
list of algorithms on one axis, a list of metrics on the other axis, and
you put in bold the algorithm that performs best. In original research
papers, it’s where you justify that your method is better than the
others, with tables such as the one below. See? It’s in bold!</p>
<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Old Classic Baseline [1]</td>
<td>0.71</td>
</tr>
<tr class="even">
<td>State-of-the-art from a few years back [2]</td>
<td>0.82</td>
</tr>
<tr class="odd">
<td>Previous work from the authors [3]</td>
<td>0.82</td>
</tr>
<tr class="even">
<td>This work [4]</td>
<td>0.84</td>
</tr>
</tbody>
</table>
<p>In a competitions, this gives us the leaderboard, which will
look something like this:</p>
<table>
<thead>
<tr class="header">
<th>Rank</th>
<th>Team</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>1</td>
<td>Big AI Research group</td>
<td>0.91</td>
</tr>
<tr class="even">
<td>2</td>
<td>Big AI Company</td>
<td>0.90</td>
</tr>
<tr class="odd">
<td>3</td>
<td>Someone with 2 GPUs</td>
<td>0.87</td>
</tr>
<tr class="even">
<td>4</td>
<td>Someone with 1 GPU</td>
<td>0.84</td>
</tr>
<tr class="odd">
<td>…</td>
<td>…</td>
<td>…</td>
</tr>
<tr class="even">
<td>157</td>
<td>I don’t know what I’m doing</td>
<td>0.42</td>
</tr>
</tbody>
</table>
<p>Needless to say, things are a bit more complicated than that. In our
new preprint, accepted at the ESANN 2025 conference and <a
href="https://research.adfoucart.be/preprint/FOUCART25_CIRank_ESANN.pdf">available
online (PDF)</a>, we argue for a more nuanced approach to ranking where,
instead of saying “this is the best method”, we compute confidence
intervals on the rankings based on the assumption that the test set is a
random sample from the larger population of “all cases where we may want
to apply our algorithm”. We take the general procedure proposed by S.
Holm in 2013<a href="#fn1" class="footnote-ref" id="fnref1"
role="doc-noteref">1</a>, which uses the result of pairwise
statistics tests to infer the confidence interval.</p>
<p>Several options for these statistical tests are evaluated using a
Monte Carlo simulation on synthetic data. The procedure that appears to
be the most robust based on our experiments is:</p>
<ol type="1">
<li>Make a Iman-Davenport test comparing the m algorithms. If the null
hypothesis (no significant difference between results) cannot be
rejected, we stop here: all algorithms have the same confidence interval
[1, m].</li>
<li>Compute pairwise one-sided Wilcoxon signed-rank tests, adjusting the
p-values using Holm’s procedure. The ranking for each algorithm is then:
[1 + #sba, m − #swa],
with sba/swa
for the number of significantly better/worse algorithms.</li>
</ol>
<p>We release with this paper the <a
href="https://gitlab.com/adfoucart/cirank">cirank</a>
Python library, which you can use to compute the confidence intervals
with:</p>
<pre
class="sourceCode python"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>from cirank import ci_ranking
<a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>import numpy # for the example
<a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>
<a href="#cb1-4" aria-hidden="true" tabindex="-1"></a># example results:
<a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>results = [np.random.random((10,)) for _ in range(5)]
<a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>
<a href="#cb1-7" aria-hidden="true" tabindex="-1"></a># default method
<a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>rankings = ci_ranking(results)
<a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>print(rankings)</pre>
<p>This paper (and the library) have large limitations in scope, and are
likely to be expanded in the future – as discussed in the paper.</p>
<p>Reference:</p>
<p>A. Foucart, A. Elskens, C. Decaestecker
Ranking the scores of algorithms with confidence
ESANN 2025 (Accepted).</p>


<ol>
<li id="fn1"><p>S.Holm. Confidence intervals for ranks. <a
href="https://www.diva-portal.org/smash/get/diva2:634016/fulltext01.pdf">https://www.diva-portal.org/smash/get/diva2:634016/fulltext01.pdf</a>,
2013.<a href="#fnref1" class="footnote-back"
role="doc-backlink">↩︎</a></p></li>
</ol>

</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2025-01-15</pubDate>
    </item><item>
    <title>Advent of code 2024</title>
    <link>https://research.adfoucart.be/advent-of-code</link>
    <description><p><a href="https://adventofcode.com/">Advent of Code</a> is a yearly
event, running <a href="https://adventofcode.com/2024/events">since
2015</a>. It is made by Eric Wastl, and it is “an Advent calendar of
small programming puzzles”.</p>
<p>I had never heard of it until this year, when I started following the
blog of <a href="https://hamatti.org/blog/">Juha-Matti Santala</a>, and
saw him document his progress. I thought it would be fun – and I was
right. I also thought that the practice of writing up how I came to the
solutions was nice, and decided to do it as well. All of my notes on the
puzzles can be found here: <a
href="https://notes.adfoucart.be/aocode24/index.html">https://notes.adfoucart.be/aocode24</a>.
And all of my code, the good and the bad, is on my <a
href="https://gitlab.com/adfoucart/aocode24">Gitlab</a>.</p>
<p>One thing I really like about it is that there are basically no rules
except the ones we bring with us. Eric posts a two-part puzzles every
day, where you have to solve the first part in order to get the second
part. You solve the puzzle by writing (or rather copy-pasting) a number
into a box, and the only check that is made is if you have the right
number. It doesn’t matter which language you used, or if your solution
is clever, or efficient, or even would generalize to any other inputs
than those he gave.</p>
<p>There is a leaderboard, but it is obviously just there for people who
really want a leaderboard. The only metric used for it is how fast you
solved the puzzle. I didn’t notice it was there until several days into
the challenge.</p>
<p>So why do I do it? I decided to use it to get better at using the
Python standard library efficiently, and to practice finding a good way
to frame the problem so that the solution is generic and modular enough
that, when part 2 is revealed, I can easily add to the existing code
without modifying too many functions (ideally, none).</p>
<p>Sometimes it works, sometimes not. We’re halfway through, and I’m
reasonably satisfied with most of my solutions so far. If you’re into
that kind of things, I highly encourage doing it. All of the puzzles
from the <a href="https://adventofcode.com/2024/events">previous
years</a> are available as well.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2024-12-12</pubDate>
    </item><item>
    <title>Brain metaphors</title>
    <link>https://research.adfoucart.be/brain-metaphors</link>
    <description><p>Over on Aeon, psychologist Robert Epstein wrote <a
href="https://aeon.co/essays/your-brain-does-not-process-information-and-it-is-not-a-computer">a
very interesting piece</a> about how we often explain the functioning of
the brain as if its a computer, retrieving memories and processing
information, despite the lack of scientific grounding for that metaphor.
Our brains don’t “recall” information, they “re-live”
it. Information is not “stored” in neurons: our brain
constantly evolved based on our experiences, and events can trigger our
brains into re-activating the areas that were activated during a
previous event, thus creating the impression of a “memory” for us.</p>
<p>This, I think, is the flip side of a discussion that I have had very
often this past year about whether AI as it exists today can qualify as
“intelligence” in any way. Just like “the computer” is not a
very good metaphor for the human brain, the human brain is not a good
metaphor for how AI algorithms work, yet this metaphor has
completely taken over the way we think about AI. In fact, the whole
vocabulary around AI is filled with human-intelligence metaphors: neural
networks, learning, AI that explain their reasoning… and obviously
“intelligence” itself. These metaphors may be useful to get a
surface-level intuition about how AI algorithms work, but they are very
much untrue and should not be taken as more than that: metaphors.</p>
<p>There’s this idea that, if we can make an artificial neural network
that is as complex as the human neural network, then we should be
capable of creating an actual intelligence. But that makes two
hypothesis which are unsupported by evidence: that artificial neural
networks are good models of human neural networks (they aren’t), and
that human neural networks are solely responsible for human
intelligence. That doesn’t seem to be the case either. Our brains work
as part of an intricate, interwoven network of systems which all
interact with each others in ways that are very hard to capture. Our
neurons without our endocrine system, our immune system, etc., are a
very incomplete snapshot of who we are. We can’t – and maybe won’t ever
be able to – upload our brains to the cloud.</p>
<p>Artificial Intelligence is not Human Intelligence, and it
doesn’t really make any sense to compare them in any way. Any
AI algorithm has capabilities and limitations, and we can study those
without falling into the trap of anthropomorphism. That’s why I don’t
like it when LLMs are compared on benchmarks such as law exams, or
common coding exercises. Those tests were designed for humans, to test
capabilities that are often difficult to acquire for human intelligence.
For an AI, it doesn’t tell us much. Importantly, it doesn’t tell us that
this AI is a good lawyer, or a good software developer. Yet these are
still the kind of tests used to compare LLMs today. Perhaps the most
used test right now is the “<a
href="https://arxiv.org/pdf/2009.03300">Massive Multitask Language
Understanding (MMLU)</a>” test. The MMLU is composed “of multiple-choice
questions from various branches of knowledge”, which were “manually
collected by graduate and undergraduate students from freely available
sources online”. But we aren’t using LLMs to solve multiple-choice
questions from various exams and tests collected around the web. For any
actual task that we have in mind for such a model, we would need to
prove that the MMLU is a useful proxy to evaluate the capability of the
model to perform the task.</p>
<p>The fact that the MMLU is collected from online sources also makes it
extremely difficult to use it as a benchmark for modern LLMs, which are
often trained on very opaque datasets, also collected from all around
the web. The risks of contamination are huge, and the measures taken by
LLM developers to mitigate this issue are often doubtful, if they are
even described at all. The <a
href="https://arxiv.org/pdf/2312.11805">Gemini</a> paper, for instance,
states that they “search for and remove any evaluation data that may
have been in our training corpus before using data for training”, but
don’t provide any detail as to how this search was done. If they used
the same method as <a href="https://arxiv.org/pdf/2303.08774">GPT-4</a>
(i.e. looking for exact matches on substrings of the questions), then
the risk of contamination is high.</p>
<p>There is often a very large gap between the performances of the LLMs
in benchmarks and their performances in real-world applications, and
this is probably part of the reason: the benchmarks are not made for the
evaluation of a computer program, but of a human brain… and those are
way too different for it to work that way.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2024-06-17</pubDate>
    </item><item>
    <title>SciSpace: a quick story</title>
    <link>https://research.adfoucart.be/scispace</link>
    <description><p>I must confess that, sometimes, I Google myself. I typically know
what I’ll find: I have enough of an online presence that most of the
results are related to me (and one other Adrien Foucart who competed in
Judo fifteen years ago), and it’s typically a mix of my blogs,
scientific papers, social media, and from time to time a post
about things I’ve written or done. Finding those is the main
reason that I do this exercise every once in a while. Today, however, I
got a surprising result, from a website that I had never heard about:
SciSpace.</p>
<p>SciSpace is yet another GPT-powered chatbot, aimed at scientists who
want to outsource their research to a machine. You ask a question, it
answers with a summary built from scientific papers, with citations to
those papers so that you can read them if you want to do some
work somewhere in the process. I don’t think it’s a good idea: doing
those kind of summaries is how you actually gain the understanding of
your field, and you’ll necessarily miss a lot of the nuance of what’s
happening in the field if you just get the AI-generated “summary”. So
even if it worked perfectly as advertised, I wouldn’t recommend using
it. But the reason I’m writing this is that it fails pretty
spectacularly at its job.</p>
<p>It seems that SciSpace allows you to <a
href="https://typeset.io/questions">browse questions</a>, presumably
asked by other users. Google indexed a question where, surprisingly, I
appeared in the answers. I say surprisingly because the question is <a
href="https://typeset.io/questions/what-are-the-specific-cultural-criticisms-associated-with-1g21907rlz">not
quite in my field</a>: “What are the specific cultural criticisms
associated with the implementation of Panopticon in various
societies?”</p>
<p>The beginning of the answer seems to be on-topic, although since it’s
a topic I know nothing about, it could all be bullshit for all I know.
But it’s around the end that I suddenly appear, with this tangent:</p>
<blockquote>
<p>Lastly, Adrien Foucart and colleagues critique the Panoptic Quality
metric in digital pathology, illustrating the challenges of applying
panoptic principles to complex, nuanced fields [10].</p>
</blockquote>
<p>This is a reference to my <a
href="https://www.nature.com/articles/s41598-023-35605-7">Scientific
Reports</a> paper “Panoptic quality should be avoided as a metric for
assessing cell nuclei segmentation and classification in digital
pathology”, which is probably the paper I like the most out of my
publications, but which has absolutely nothing to do with the
Panopticon, outside of the fact that there is “Panoptic” in the
title.</p>
<p>And this, I think, is where GPT went wrong. Because GPT predicts
tokens, which encode parts of words, “Panoptic” and
“Panopticon” likely share one or several tokens in common. This alone
shouldn’t be enough to trip the model, but it also happens that one of
the names most associated with discussion of the Panopticon is Michel
Foucault, and “Foucault” also shares at least one token with “Foucart”.
This taken together probably sent SciSpace’s GPT into the wrong
direction. Because, as we should all know by now, GPT has no
fucking idea what it’s talking about. It’s all just a
statistically likely string of tokens, with no understanding whatsoever
of what’s going on.</p>
<p>So Foucault and the Panopticon end up mixed with Foucart and Panoptic
Quality, even though these come from completely different domains. I
should also note that for some reason my paper is mistakenly cited as
coming from “Dental science reports” instead of “Scientific Reports”. No
idea what happened there, but another clear reason that this tool is
absolutely useless.</p>
<p>From the CEO’s <a
href="https://www.linkedin.com/in/saikiranchandha/">LinkedIn</a>
profile, the platform is “used by more than a million researchers
worldwide, including Nobel Laureates”. I highly doubt, however, that
Nobel prize worthy science will be done with it anytime soon.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2024-06-15</pubDate>
    </item><item>
    <title>ChatGPT on evaluation metrics</title>
    <link>https://research.adfoucart.be/chatgpt-metrics</link>
    <description>
<p>I recently reviewed a paper which I strongly suspect, without certainty, had a little help from ChatGPT for its research. One thing that tipped me off was that it was bit confused in the evaluation metrics: it reported, for a segmentation task, both the “F1” score and the “Dice” score, which it defined as:</p>
<p>$$F1 = \frac{2 \times precision \times recall}{precision + recall}$$</p>
<p>$$Dice = \frac{|P \cap G|}{|P| + |G|}$$</p>
<p>Where P &amp; G refer to the predicted and ground truth regions.</p>
<p>Both of these definitions are fine, but… well, before I explain the problem, let’s see what ChatGPT (version 3.5) has to say about it.</p>
<h2 id="asking-the-question">Asking the question</h2>
<pre>User:  
What is the difference between the F1 score and the Dice Similarity Coefficient ?</pre>
<p>ChatGPT’s answer:</p>
<p>The F1 score and the Dice Similarity Coefficient (also known as Dice coefficient or Dice index) are both metrics commonly used to evaluate the performance of binary classification tasks, particularly in the context of image segmentation or medical imaging. While they serve similar purposes, they have some differences in how they are calculated and what aspects of the classification performance they emphasize.</p>
<ol type="1">
<li>F1 Score:</li>
</ol>
<ul>
<li>The F1 score is the harmonic mean of precision and recall.</li>
<li>Precision measures the ratio of true positive predictions to the total number of positive predictions made by the classifier.</li>
<li>Recall measures the ratio of true positive predictions to the total number of actual positive instances in the dataset.</li>
<li>The F1 score balances both precision and recall, giving equal weight to both. It is calculated as: F1 = 2 * (precision * recall) / (precision + recall).</li>
<li>F1 score ranges from 0 to 1, where a higher value indicates better performance.</li>
</ul>
<ol start="2" type="1">
<li>Dice Similarity Coefficient:</li>
</ol>
<ul>
<li>The Dice coefficient measures the overlap between two samples.</li>
<li>It is calculated as twice the intersection of the two samples divided by the sum of the sizes of the two samples.</li>
<li>In the context of binary classification tasks, one sample typically represents the predicted segmentation mask, while the other represents the ground truth mask.</li>
<li>Dice coefficient ranges from 0 to 1, where 0 indicates no overlap between the predicted and ground truth masks, and 1 indicates perfect overlap.</li>
</ul>
<p>Differences</p>
<ul>
<li>While both metrics are used to evaluate binary classification tasks, they emphasize different aspects of classification performance.</li>
<li>F1 score emphasizes the balance between precision and recall, which is particularly useful when there is an imbalance between the classes in the dataset.</li>
<li>Dice coefficient emphasizes the overlap between the predicted and ground truth masks, which is especially relevant in tasks such as image segmentation, where the spatial agreement between the predicted and true regions is crucial.</li>
<li>The F1 score can penalize false positives and false negatives equally, while the Dice coefficient may be less sensitive to false negatives if the overlap between the predicted and true regions is still significant.</li>
</ul>
<p>In summary, both metrics are valuable tools for evaluating classification performance, but the choice between them depends on the specific requirements and characteristics of the task at hand.</p>
<h2 id="dissecting-the-answer">Dissecting the answer</h2>
<p>ChatGPT is verbose as always, but the two definitions are mostly correct. Nothing much to say about the F1-Score, and for Dice the only real problem I have is with “measures the overlap,” as I’d rather say “is one of the measures of overlap.” The other main one being the Jaccard Index, also known as “Intersection over Union.”</p>
<p>The “differences” section is where I have way more problems. F1 score is balanced in terms of precision and recall, but it is not at all appropriate when there is a class imbalance (see <a href="https://research.adfoucart.be/thesis/4-emp.html#imbalanced-datasets-in-classification-tasks">Chapter 4 of my thesis</a> for more!). And “the Dice coefficient may be less sensitive to false negatives if the overlap between the predicted and true regions is still significant” is nonsense.</p>
<p>But that’s still not the main issue. Let’s see if I can get ChatGPT to notice it.</p>
<h2 id="one-step-further">One step further</h2>
<pre>User:  
Express both the F1 score and the Dice Similarity Coefficient in terms of True Positives, False Positive and False Positives.</pre>
<p>ChatGPT’s answer</p>
<p>Sure, here are the formulas for both the F1 score and the Dice Similarity Coefficient expressed in terms of True Positives (TP), False Positives (FP), and False Negatives (FN):</p>
<ul>
<li>F1 Score:
<ul>
<li>Precision (P) = TP / (TP + FP)</li>
<li>Recall (R) = TP / (TP + FN)</li>
<li>F1 Score = 2 * (P * R) / (P + R)</li>
</ul></li>
<li>Dice Similarity Coefficient:
<ul>
<li>Dice = (2 * TP) / (2 * TP + FP + FN)</li>
</ul></li>
</ul>
<p>In both cases, TP represents the number of true positive predictions, FP represents the number of false positive predictions, and FN represents the number of false negative predictions. These formulas allow us to calculate both metrics based on the counts of these classification outcomes.</p>
<h2 id="almost-there">Almost there</h2>
<p>The formulas are still correct ! But weirdly, ChatGPT didn’t exactly do as I asked. It still puts the F1 Score in terms of Precision and Recall, not in terms of TP, FP and FN. Let’s prompt it a bit further.</p>
<pre>User:  
You put the F1 Score in terms of P and R, give the complete formula for the F1 Score based on TP, FP and FN.</pre>
<p>ChatGPT’s answer</p>
<p>Apologies for the oversight. Here’s the complete formula for the F1 score expressed in terms of True Positives (TP), False Positives (FP), and False Negatives (FN):</p>
<p>F1 Score = (2 * TP) / (2 * TP + FP + FN)</p>
<p>This formula directly uses the counts of true positives, false positives, and false negatives to compute the F1 score, without needing to calculate precision and recall separately.</p>
<h2 id="there-we-go">There we go !</h2>
<p>So let’s put the two formulas next to each other:</p>
<p>$$Dice = \frac{2 \times TP}{2 \times TP + FP + FN}$$</p>
<p>$$F1 Score = \frac{2 \times TP}{2 \times TP + FP + FN}$$</p>
<p>They are the same thing ! Dice and F1 Score are two different names for the same thing. The only real difference is in when they are often used. F1 is a more common terminology in classification and detection problems, whereas Dice is more often used for segmentation problems. But they are the same.</p>
<p>All the talk about their differences was complete bullshit. But it would have been relatively difficult to spot without already knowing the information. Which is always the problem with trying to use ChatGPT as a source of knowledge.</p>
<h2 id="conclusions">Conclusions</h2>
<p>It was not a very good paper, and whether the authors misunderstood evaluation metrics because of ChatGPT or all on their own, I don’t think their study will be published, at least in the journal they were aiming for.</p>
<p>But after more than a year of people trying to convince me that ChatGPT really is usually right, and useful as a source of information… I remain firmly convinced that it’s only “right” if you don’t know the topic and can’t fact-check the answer fully. In this case, the information is easily found on Google (even on Bing !). On Google, the first line I see with the same original prompt is: “Dice coefficient = F1 score,” from a <a href="https://www.kaggle.com/code/nghihuynh/understanding-evaluation-metrics-in-segmentation">Kaggle notebook</a>.</p>
<p>Sure, “if I upgrade to the paid version” it may be better. The only thing that really makes ChatGPT-Plus better is the capacity to search the web. I’d rather do that directly on my own – and see the information in its context, so that I can critically assess it.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2024-02-09</pubDate>
    </item><item>
    <title>Retrieving TCGA slides with Python and the GDC API</title>
    <link>https://research.adfoucart.be/tcga-retrieval-gdc-api</link>
    <description><p>Note: the code for all of this is available on Gitlab here: <a href="https://gitlab.ulb.be/lisa/tcgasampler">https://gitlab.ulb.be/lisa/tcgasampler</a>. It may even perhaps work.</p>
<h2 id="the-goal">The goal</h2>
<p>I would like to have a relatively big stock of <a href="https://portal.gdc.cancer.gov/">TCGA</a> whole-slide images, from various tissue locations and cancer types, and at a relatively low resolution, so that I can quickly test algorithms such as <a href="https://research.adfoucart.be/preprint-tissue-segmentation">tissue segmentation</a> or artifact detection that generally don’t require super-high magnification.</p>
<p>The National Cancer Institute provides an <a href="https://gdc.cancer.gov/developers/gdc-application-programming-interface-api">API</a> to access their datasets programatically, so this is what I’d like to do:</p>
<ul>
<li>Get the list of all the diagnostic tissue slides.</li>
<li>Randomly select N slides.</li>
<li>Download and save a copy of those slides at at chosen resolution.</li>
</ul>
<h2 id="accessing-image-data">Accessing image data</h2>
<p>There are basically two ways to access image data for a given slide:</p>
<ul>
<li>Downloading the entire whole-slide image using the “data endpoint” of the API: https://api.gdc.cancer.gov/data/{file_id}</li>
<li>Directly loading the tiles from their tile server, used by their online slide viewer: https://portal.gdc.cancer.gov/auth/api/v0/tile/{file_id}?level={level}&amp;x={x}&amp;y={y}.</li>
</ul>
<p>Both options come with their own set of problems.</p>
<p>In the first case, we have to download a full-resolution slide, which is often 1 GB or more, just to extract the low-resolution image from it (and then delete the full WSI to avoid filling up local storage too quickly).</p>
<p>In the second case… we don’t have access to any of the metadata of the slide, including, crucially, the resolution and magnification level. This means that we have to kinda guess at which “level” – as understood by the slide server – we have to work. Of course, the “levels” of the slide server are not the same as the “levels” of the image pyramid in the slide file. But at least we only download the data that we want, which is a lot faster.</p>
<p>So I’ve tried both: with the first option, I can have exactly the resolution I want for all images, but it takes a ton of time; in the second option, it’s faster but I will have variations in the resolution.</p>
<h2 id="the-overall-pipeline">The overall pipeline</h2>
<p>We start by setting a random seed for repeatability, and then we load and shuffle the file ids from the manifest:</p>
<pre class="sourceCode python"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>random.seed(RANDOM_SEED)
<a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>with open(PATH_TO_MANIFEST, &#39;r&#39;) as fp:
<a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>    all_tissue_slides = [line.strip().split(&#39;\t&#39;) for line in fp.readlines()]
<a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>
<a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>rows = all_tissue_slides[1:]
<a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>row_ids = list(range(len(rows)))
<a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>random.shuffle(row_ids)</pre>
<p>We can then iterate through the row_ids to get the file_id and filename for each slide. This will randomly sample through the full TCGA dataset.</p>
<h2 id="option-a-downloading-full-slides">Option A: downloading full slides</h2>
<p>In the first option, we use the data endpoint to load the whole-slide file and save it to the disk:</p>
<pre class="sourceCode python"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>response = requests.get(data_endpt, headers={&quot;Content-Type&quot;: &quot;application/json&quot;})
<a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>with open(os.path.join(f&quot;{GDC_DATA_ENDPT}/{file_id}&quot;, f&quot;tmp/{filename}&quot;), &quot;wb&quot;) as fp:
<a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>    fp.write(response.content)</pre>
<p>Then we can use our WholeSlide class (available in <a href="https://pypi.org/project/openwholeslide/">our openwholeslide package</a>) to extract the image at the target resolution:</p>
<pre class="sourceCode python"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>wsi = WholeSlide(os.path.join(TCGA_LOCAL_PATH, f&quot;tmp/{filename}&quot;))
<a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>region = wsi.read_full(resolution=TARGET_RESOLUTION)
<a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>imsave(os.path.join(TCGA_LOCAL_PATH, f&quot;images/{filename}.png&quot;), region.as_ndarray)
<a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>wsi.slide.close()</pre>
<p>And finally we remove the downloaded file, so that the disk doesn’t fill up:</p>
<pre class="sourceCode python"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>os.remove(os.path.join(TCGA_LOCAL_PATH, f&quot;tmp/{filename}&quot;))</pre>
<h2 id="option-b-downloading-tiles">Option B: downloading tiles</h2>
<p>Getting a tile from the GDC API requires to provide the file id – which we have from the manifest – alongside the level, and an “x-y” position. The “x-y” position is a tile index, so that the first tile is (x=0, y=0), then the one to the right is (x=1, y=0), etc. The level is related to the size of the image. It’s not really documented, but from playing a bit with the API it’s clear that the system is basically just starting from a 1x1px at level 0, then increasing by powers of 2. At some point it diverges from the expected 2x2, 4x4, 8x8, etc., so that the real aspect ratio is preserved. So for instance for the image with id <a href="https://portal.gdc.cancer.gov/files/455d9335-c6f3-4966-8b3c-1291e2d31093">455d9335-c6f3-4966-8b3c-1291e2d31093</a>, we have:</p>
<ul>
<li>level 0: 1x1</li>
<li>level 1: 2x2</li>
<li>level 2: 4x3</li>
<li>level 3: 7x6</li>
<li>level 4: 14x11 …</li>
<li>level 9: 428x351</li>
</ul>
<p>We can get some metadata from the API using: <a href="https://portal.gdc.cancer.gov/auth/api/v0/tile/metadata/455d9335-c6f3-4966-8b3c-1291e2d31093">https://portal.gdc.cancer.gov/auth/api/v0/tile/metadata/455d9335-c6f3-4966-8b3c-1291e2d31093</a>. It tells us the full size of the image at maximum resolution (here 109559x89665), and the tile size (here 512). So we know that above level 9, we will start to have multiple tiles.</p>
<p>Let’s try to grab the images at a resolution such that the image is, at most, 2048px in its largest dimension. For that, we need to use level 11. But first, we need to grab the metadata to get the aspect ratio and have an idea of the total size of the resulting image.</p>
<pre class="sourceCode python"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>meta_url = f&quot;{GDC_META_ENDPT}/{file_id}&quot;
<a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>response_meta = requests.get(meta_url)
<a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>metadata = json.loads(response_meta.content)
<a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>full_width = int(metadata[&#39;Width&#39;])
<a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>full_height = int(metadata[&#39;Height&#39;])
<a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>overlap = int(metadata[&#39;Overlap&#39;])
<a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>tile_size = int(metadata[&#39;TileSize&#39;])
<a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>
<a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>if full_width &gt; full_height:
<a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>    expected_width = 2048
<a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>    expected_height = math.ceil(2048 * full_height/full_width)
<a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>else:
<a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>    expected_width = math.ceil(2048 * full_width / full_height)
<a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>    expected_height = 2048
<a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>
<a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>downloaded_image = np.zeros((expected_height, expected_width, 3), dtype=np.uint8)</pre>
<p>We will then use the tile_size to compute the maximum number of tiles per dimension, and slowly fill the image from the tiles. We also keep track in max_x and max_y of the real size of the resulting image so that we can crop it from the temporary black image at the end.</p>
<pre class="sourceCode python"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>tile_url = lambda x, y: f&quot;{GDC_TILE_ENDPT}/{file_id}?level=11&amp;x={x}&amp;y={y}&quot;
<a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>max_tiles = (2048 // (tile_size-overlap)) + 1
<a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>max_x = 0
<a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>max_y = 0
<a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>for y in range(max_tiles):
<a href="#cb6-7" aria-hidden="true" tabindex="-1"></a>    for x in range(max_tiles):
<a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>        response_img = requests.get(tile_url(x, y))
<a href="#cb6-9" aria-hidden="true" tabindex="-1"></a>        if response_img.ok:
<a href="#cb6-10" aria-hidden="true" tabindex="-1"></a>            image = imread(BytesIO(response_img.content))
<a href="#cb6-11" aria-hidden="true" tabindex="-1"></a>            startx = x*(tile_size-overlap)
<a href="#cb6-12" aria-hidden="true" tabindex="-1"></a>            starty = y*(tile_size-overlap)
<a href="#cb6-13" aria-hidden="true" tabindex="-1"></a>            downloaded_image[starty:starty+image.shape[0], startx:startx+image.shape[1]] = image[..., :3]
<a href="#cb6-14" aria-hidden="true" tabindex="-1"></a>            max_x = max(max_x, startx+image.shape[1])
<a href="#cb6-15" aria-hidden="true" tabindex="-1"></a>            max_y = max(max_y, starty+image.shape[0])
<a href="#cb6-16" aria-hidden="true" tabindex="-1"></a>
<a href="#cb6-17" aria-hidden="true" tabindex="-1"></a>imsave(down_path, downloaded_image[:max_y, :max_x])</pre>
<h2 id="conclusions">Conclusions</h2>
<p>With Option A, it took me about 11 hours to get 100 random images at a resolution of 15µm/px – but at least I know their resolution. It took only 2 hours to get the same amount of images with option B… but with varying levels of resolution.</p>
<p>In the end, I think both options can be useful ways of randomly sampling TCGA data without storing the full WSIs locally. Which, given their 12.95TB size, I’d rather not do.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-10-06</pubDate>
    </item><item>
    <title>Javascript's weird typecasting rules</title>
    <link>https://research.adfoucart.be/javascript-weird-concatenation</link>
    <description><p>Over on Instagram, I saw <a href="https://www.instagram.com/p/CvXy1MGILjb/">a post</a> making fun of Javascript’s weird and unpredictable (unless you really know Javascript) typecasting behaviour. There is a very long tradition of mocking Javascript, with perhaps the most famous instance being the “<a href="https://www.destroyallsoftware.com/talks/wat">Wat</a>” video from Gary Bernhardt, complete with very dated early 2010s meme culture (I feel old).</p>
<p>Neither really take the time to explain what’s going on, so I thought it would be interesting to quickly go through the Instagram one, because I think it’s actually interesting to illustrate the difficulty of creating a programming language that intuitively “makes sense.” Something which Javascript spectacularly fails to do.</p>
<p>So, first one:</p>
<pre class="sourceCode javascript"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>console.log(3 + 3);</pre>
<pre class="console">6</pre>
<p>This, obviously, is the expected behaviour. Nothing to see.</p>
<pre class="sourceCode javascript"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>console.log(&quot;3&quot; + &quot;3&quot;);</pre>
<pre class="console">33</pre>
<p>This is logical as well, but it clearly shows the root of the issue with a “dynamic type” language. Because the + addition operator means “arithmetic addition” (for numbers), but also “concatenation” (for strings), the creators of the language have to make some choices as to what happens when you mix both types. Which is where it starts to become weird.</p>
<pre class="sourceCode javascript"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>console.log(&quot;3&quot; + + 3);</pre>
<pre class="console">33</pre>
<p>In this case, the + + is not actually needed, we could just use a single + and get the same result. It’s just put there to setup the next bit. But the reasoning here is simple: we start from a string, we have a + so we want to concatenate something. What is this thing? + 3, which is a number. But we cannot concatenate a number to a string, so it is first cast into the string 3, hence the final result: the string 33.</p>
<pre class="sourceCode javascript"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>console.log(3 + + &quot;3&quot;);</pre>
<pre class="console">6</pre>
<p>This is where it starts being a bit confusing, especially since doing 3 + "3" actually outputs 33. So the casting is done towards the string type for "3" + 3 and for 3 + "3", but the second + signs causes everything to fall back to numbers. Why? Because + in Javascript isn’t just for addition or concatenation, it can also be the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Unary_plus">unary plus operator</a>, which converts whatever follows it to a number. And, because the unary operator has <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence">precedence</a> over the addition operator, what Javascript evaluates is:</p>
<ol type="1">
<li>a = + "3" = 3</li>
<li>3 + a = 3 + 3 = 6</li>
</ol>
<pre class="sourceCode javascript"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>console.log(3 + 3 - 3);</pre>
<pre class="console">3</pre>
<p>No problem here.</p>
<pre class="sourceCode javascript"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>console.log(&quot;3&quot; + &quot;3&quot; - &quot;3&quot;);</pre>
<pre class="console">30</pre>
<p>What’s happening here ? The two operators, + (addition because there are things on both sides, not unary plus) and - have the same precedence, and arithmetic operators use “left-associativity” (except exponentiation, but let’s not go there), so this gets evaluated as ("3" + "3") - "3".</p>
<p>With "3" + "3", + will mean “concatenation,” so we get the string 33. Then, we subtract the string 3. But while addition applied to strings is concatenation, subtraction applied to string… doesn’t exist in javascript. The subtraction operator always try to “<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Subtraction">coerce both operands to numeric values</a>” before performing the operation, so "33" - "3" is evaluated as 33 - 3 = 30.</p>
<p>How do other languages handle it ? Typically, most language prefer to throw an exception rather than to have unintuitive outputs. Python for instance, will refuse to do "3" + 3 or 3 + "3", throwing a TypeError in both cases. It does have its own weirdness, however, as "3" * 3 will work, and output the string '333': the multiplication operator, for strings, is interpreted as a “repeat” operator in Python.</p>
<p>So what’s the conclusion of all that ? Except for “don’t use Javascript unless you really, really have to,” it’s: programming languages are often weird, but in the end they are just following exactly their internal rules. I certainly prefer languages where those rules tend to produce the behaviour that I intuitively expect. But languages are designed according to their own needs. Javascript tries to keep going as long as possible without crashing, because the goal is for end users on their web browsers to not have any errors on their ends. “<a href="https://blog.codinghorror.com/javascript-and-html-forgiveness-by-default/">Forgiveness by Default</a>,” because if it doesn’t, users are unhappy.</p>
<p>Also, developpers have been making fun of Javascript for so long that it would now feel weird if it became less weird.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-10-03</pubDate>
    </item><item>
    <title>[Preprint] Finding the best channel for tissue segmentation in whole-slide images</title>
    <link>https://research.adfoucart.be/preprint-tissue-segmentation</link>
    <description><p>Tissue segmentation in digital pathology is one of these weird little problems where it seems like it should be trivial, where in fact in many situations it is trivial, yet if you need a robust solution with a reasonable amount of precision, it is suprisingly difficult to get it right. This is something that we examine in our latest preprint, accepted into the SIPAIM 2023 conference [<a href="./pub-html/FOUCART23-TissueSegmentation.html">read as HTML</a> - <a href="./preprint/FOUCART23-TissueSegmentation.pdf">read as PDF</a>], written with Arthur Elskens, Olivier Debeir and Christine Decaestecker.</p>
<h2 id="what-are-we-trying-to-do">What are we trying to do?</h2>
<p>This is a situation that most developpers who had to work with digital pathology images had to deal with at some point or another: you have a very large image with some tissue regions, and a lot of light gray “background,” where the backlight from the microscope shines through the glass slide.</p>

<img src="./img/tcga-wsi.png" alt="Exemple of a whole-slide image from a breast cancer sample (source: TCGA, case TCGA-AC-A2FB)" />Exemple of a whole-slide image from a breast cancer sample (source: <a href="https://portal.gdc.cancer.gov/cases/02bbb632-0f7f-439d-b8f0-c86a06237424">TCGA, case TCGA-AC-A2FB</a>)

<p>The goal is to clearly separate the tissue region from the background region. There are many potential reasons for that. One of the most common is when we are extracting patches for training a deep neural network: we don’t want to extract tiles in the background region, where there is no useful information to be found.</p>
<p>Another case is when we are trying to quantify stuff, such as the percentage of the tissue that is covered by, for instance, tumoral cells. It’s important in that case to have a good estimation of what the total tissue area is. Kleczek et al.<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref">1</a> also uses tissue segmentation to get a more accurate estimation of the stain colors, for a process called stain deconvolution which is also fairly common.</p>
<p>Those different cases have very different requirements: for the patch extraction, for instance, we don’t really care if it’s super precise. For quantification processes, however, we do.</p>
<h2 id="how-do-we-do-it">How do we do it?</h2>
<p>When we look at the litterature, it’s quickly apparent that most people just kinda wing it (which is what I was doing up to know as well, to be honest). Everyone has a slightly different but relatively similar pipeline, which most of the time can be summed up in a few steps:</p>
<ul>
<li>Channel reduction: convert the 3-channel color image into a single-channel “grayscale” image.</li>
<li>Thresholding: determine the best value that separates background from non-background in this single-channel image.</li>
<li>Post-procsesing: clean-up the segmentation mask, typically with some morphological operations and by filling any holes within the tissue region.</li>
</ul>
<p>A few algorithms have also been developped a bit more rigorously, such as FESI<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref">2</a> or EntropyMasker<a href="#fn3" class="footnote-ref" id="fnref3" role="doc-noteref">3</a>, but they still follow mostly the same pipeline.</p>
<h2 id="our-study">Our study</h2>
<p>The steps that seems the most important in this process is the channel reduction step: how do we transform the image so that the value of the pixels in the single-channel image are well-correlated with whether they are within the tissue region or not.</p>
<p>The main characteristic of the background region is that it’s light, so a common choice is to simply use the regular grayscale transform (or “inverted” grayscale so that tissue regions appear brighter than background regions). Another characteristic is that it’s grayish, or “desaturated.” So we can use the saturation in the HSV colorspace. Another approach (used for instance in the VALIS registration software<a href="#fn4" class="footnote-ref" id="fnref4" role="doc-noteref">4</a>) is to first estimate the color of the background region (by looking at the brightest pixels), then compute a color distance from each pixel to this estimated background color. Pixels in the tissue should be “further” in colorspace.</p>
<p>We can also use the other main charactersitic of the background region: that it’s homogeneous. This is the approach from EntropyMasker: to use the local entropy – or, in other works, the Laplacian or the output of an edge detection algorithm – which will be higher in “textured” regions (such as the tissue) and lower in the background regions.</p>
<p>The goal of our study was to evaluate whether one or some of those approaches, all commonly found in the litterature, are better than the others.</p>
<h2 id="our-results">Our results</h2>
<p>We use annotated data from Bándi et al.<a href="#fn5" class="footnote-ref" id="fnref5" role="doc-noteref">5</a> and from the TiGER challenge<a href="#fn6" class="footnote-ref" id="fnref6" role="doc-noteref">6</a>. Five slides from Bándi and 62 from TiGER are used as a development / learning set. Five slides from Bándi and 31 from TiGER as a test set. We worked using a low magnification level corresponding to a resolution of around 15µm/px in all experiments.</p>
<p>We first analyzed the separability of the tissue and background value distributions in the different single-channel options on the development set, and find that the local entropy provides the best separability, and the grayscale the worst.</p>
<p>We then look at the results of a basic pipeline of channel reduction, median filter, thresholding and morphological post-processing, and compare the four channels’ results according to the Intersection over Union metric. For the thresholding step, we use either a “learned” threshold (based on the development set), or the Otsu algorithm. FESI and EntropyMasker results are also tested for comparison.</p>

<img src="./img/sipaim_results.png" alt="Results on a representative image of the test set. The contours of the masks are overlaid on the RGB image (for the annotations, FESI and EntropyMasker) and on the single-channel representations (for the fixed (FT) and Otsu thresholds)." />Results on a representative image of the test set. The contours of the masks are overlaid on the RGB image (for the annotations, FESI and EntropyMasker) and on the single-channel representations (for the fixed (FT) and Otsu thresholds).

<p>Our main conclusions are:</p>
<blockquote>
<p>Our results demonstrate that transforming the RGB image to grayscale, as is very commonly done for tissue segmentation, is a suboptimal choice that should generally be avoided. Better channel reduction choices are available, focusing either on the colorization of the tissue compared to the background (e.g. with Saturation or ColorDist), or on the texture (e.g. with LocalEntropy). Pipelines with many handcrafted steps such as the one proposed in EntropyMasker may be unecessarily complex, as a very simple pipeline obtain very similar results as long as the right channel reduction step is applied.</p>
</blockquote>
<h2 id="what-about-deep-learning">What about deep learning ?</h2>
<p>Tissue segmentation can certainly also be performed using deep convolutional neural networks. Bándi et al., for instance, showed very good results using U-Net. We decided against including such methods in our analysis, however:</p>
<blockquote>
<p>We purposefully chose not to focus on deep learning solutions in this study. Often, one of the objectives of tissue segmentation is to have a very simple and quick method that offer a result that doesn’t need to be pixel-perfect. Making simple, classical pipelines more robust and reliable can allow us to get good results while wasting less computing resources.</p>
</blockquote>
<p>I think that, in general, we should try to find the “simplest” solution to a problem. For my future needs in tissue segmentation, I’d much rather have a robust, reliable pipeline based on channel reduction and basic operations (that can easily be translated to any framework and included in any pipeline without much fuss) rather than to have a big trained neural network that will require some heavy maintenance to keep up to date with the latest versions of deep learning libraries. I have some neural networks that were trained two years ago and which are already annoyingly complicated to run now. I’d feel a lot more comfortable with the kind of pipeline that we studied here.</p>
<h2 id="so-do-we-have-a-robust-solution">So do we have a robust solution?</h2>
<p>First we have to refine the other steps of the pipline, most particularly the post-processing, and to potentially combine different thresholds. The homemade pipeline that I used until now thresholded on a combination of the grayscale and saturation channels. I’ll likely change that to rather use saturation and color distance or saturation and local entropy.</p>
<p>The code for our experiments is available on Gitlab: <a href="https://gitlab.com/adfoucart/tissue-segmentation">https://gitlab.com/adfoucart/tissue-segmentation</a></p>


<ol>
<li id="fn1" role="doc-endnote"><p>Kleczek et al., 2020. A novel method for tissue segmentation in high-resolution H&amp;E-stained histopathological whole-slide images. <a href="https://doi.org/10.1016/j.compmedimag.2019.101686">https://doi.org/10.1016/j.compmedimag.2019.101686</a><a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2" role="doc-endnote"><p>Bug et al., 2015. Foreground Extraction for Histopathological Whole Slide Imaging. <a href="https://doi.org/10.1007/978-3-662-46224-9_72">https://doi.org/10.1007/978-3-662-46224-9_72</a><a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3" role="doc-endnote"><p>Song et al., 2023. An automatic entropy method to efficiently mask histology whole-slide images. <a href="https://doi.org/10.1038/s41598-023-29638-1">https://doi.org/10.1038/s41598-023-29638-1</a><a href="#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn4" role="doc-endnote"><p>Gatenbee et al., 2021. VALIS: Virtual Alignment of pathoLogy Image Series. <a href="https://doi.org/10.1101/2021.11.09.467917">https://doi.org/10.1101/2021.11.09.467917</a><a href="#fnref4" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn5" role="doc-endnote"><p>Bándi et al., 2017. Comparison of different methods for tissue segmentation in histopathological whole-slide images. <a href="https://doi.org/10.1109/ISBI.2017.7950590">https://doi.org/10.1109/ISBI.2017.7950590</a><a href="#fnref5" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn6" role="doc-endnote"><p><a href="https://tiger.grand-challenge.org/">https://tiger.grand-challenge.org/</a><a href="#fnref6" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>

</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-09-25</pubDate>
    </item><item>
    <title>Authors suing OpenAI</title>
    <link>https://research.adfoucart.be/authors-v-openai</link>
    <description><p>Two authors, Paul Tremblay and Mona Awad, have filed a lawsuit against OpenAI for “direct copyright infringement, vicarious copyright infringement, violations of section 1202(b) of the Digital Millenium Copyright Act, unjust enrichment, violations of the California and common law unfair competition laws, and negligence”<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref">1</a></p>
<p>When I first heard about the complaint, I was a little bit skeptical. The key element of the complaint (from what I saw in the media coverage<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref">2</a>) was that ChatGPT was capable of generating very accurate summaries of those works. In the complaint itself, we can read in the Overview section:</p>
<blockquote>
<p>Indeed, when ChatGPT is prompted, ChatGPT generates summaries of Plaintiffs’ copyrighted works—something only possible if ChatGPT was trained on Plaintiffs’ copyrighted works.</p>
</blockquote>
<p>And later on in the Factual Allegations section:</p>
<blockquote>
<p>On information and belief, the reason ChatGPT can accurately summarize a certain copyrighted book is because that book was copied by OpenAI and ingested by the underlying OpenAI Language Model (either GPT-3.5 or GPT-4) as part of its training data.</p>
</blockquote>
<p>While I have few doubts that OpenAI is, indeed, using a ton of copyrighted work to train its models, this doesn’t really prove it in my opinion. It seems to imply that the language model is really summarizing works that it has somehow “ingested” in its model. But GPT isn’t really summarizing anything, of course: it’s doing its best impression of a summary of those books, which, as the complaint notes, “get some details wrong.” I find it a lot more likely that, when prompted to summarize those books, GPT is influenced more by online summaries of the books than by the books themselves.</p>
<p>Looking more closely at the full complaint and the exhibits<a href="#fn3" class="footnote-ref" id="fnref3" role="doc-noteref">3</a>, however, I think it’s a lot more solid than it initially appears. While Tremblay and Awad use their own books as example, their complaint is much bigger, as its presented as a class action on behalf of “all persons or entities domiciled in the United States that own a United States copyright in any work that was used as training data for the OpenAI Language Models.” And their case for why OpenAI’s training data includes copyrighted works is largely based off information from OpenAI themselves.</p>
<p>In particular, they note that:</p>
<ul>
<li>GPT-1 used BookCorpus, which is copied from Smashwords.com and is – apparently – known to contain work under copyright.</li>
<li>GPT-3 adds “two internet-based books corpora,” whose nature is unspecified, but which according to OpenAI’s papers would contain respectively around 60.000 titles and 300.000 titles.</li>
</ul>
<p>The thing is, there are not really many sources of book data with so many works in it. The suit identifies two likely candidates. The 60k books dataset could correspond to Project Gutenberg. Those books are no longer under copyright, so that’s fine. But the only internet-based books repositories that could include the 300k books from the second dataset would be the LibGen / Z-Lib / Sci-Hub / Bibliotik corpus and/or torrent collections of books, which mostly include copyrighted material.</p>
<p>I think that’s the strongest argument they have here: based off OpenAI’s claim on their own dataset, they almost certainly used copyrighted material to train their model. And if OpenAI wants to demonstrate that it’s not the case, they may have to give a bit more information about their data… So we may finally get some openness from them – under oath.</p>
<p>This is not just about those two authors, however. The lawyers behind this, Joseph Saveri and Matthew Butterick, are clearly determined to make life very difficult for OpenAI and other generative AI startups. Three other authors (Sarah Silverman, Chris Golden and Richard Kadrey) joined an identical complaint against OpenAI<a href="#fn4" class="footnote-ref" id="fnref4" role="doc-noteref">4</a> and against Meta for their own language model, LLaMA<a href="#fn5" class="footnote-ref" id="fnref5" role="doc-noteref">5</a>. LLaMa is apparently trained on “the Books3 section of ThePile”<a href="#fn6" class="footnote-ref" id="fnref6" role="doc-noteref">6</a>. That particular dataset comes from EleutherAI<a href="#fn7" class="footnote-ref" id="fnref7" role="doc-noteref">7</a>, and Books3 is described as coming from Bibliotik, compiled by Shawn Presser and shared on Twitter<a href="#fn8" class="footnote-ref" id="fnref8" role="doc-noteref">8</a>. Since this dataset is easy to find and download, this is a lot more straightforward: copyrighted books from the authors filing the complaint are in the Books3 dataset, so – according to Meta’s own papers – they were used in training LLaMA. Saveri and Butterick are also behind the lawsuits against GitHub Copilot<a href="#fn9" class="footnote-ref" id="fnref9" role="doc-noteref">9</a> and Stable Diffusion<a href="#fn10" class="footnote-ref" id="fnref10" role="doc-noteref">10</a>.</p>
<p>It will be interesting to see what kind of precedent the US courts decide to set here. The claim that the training sets contain copyrighted material may be strong, but that doesn’t necessarily mean that training a language model with copyrighted material is by itself infrigement. What the judges will decide to do with all these information, I don’t know. I’m sure many lawyers are going to fight about it for many years, though.</p>


<ol>
<li id="fn1" role="doc-endnote"><p><a href="https://llmlitigation.com/pdf/03223/tremblay-openai-complaint.pdf">Tremblay v OpenAI complaint</a><a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2" role="doc-endnote"><p><a href="https://www.theguardian.com/books/2023/jul/05/authors-file-a-lawsuit-against-openai-for-unlawfully-ingesting-their-books">The Guardian, July 5th, 2023</a><a href="#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3" role="doc-endnote"><p><a href="https://llmlitigation.com/pdf/03223/tremblay-openai-complaint-exhibits.pdf">Tremblay v OpenAI Exhibits (PDF)</a><a href="#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn4" role="doc-endnote"><p><a href="https://llmlitigation.com/pdf/03416/silverman-openai-complaint.pdf">Silverman v OpenAI complaint</a><a href="#fnref4" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn5" role="doc-endnote"><p><a href="https://llmlitigation.com/pdf/03417/kadrey-meta-complaint.pdf">Kadrey v Meta complaint</a><a href="#fnref5" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn6" role="doc-endnote"><p>Touvron et al., LLaMA: Open and Efficient Foudation Language Models, 2023. <a href="https://arxiv.org/abs/2302.13971">ArXiV:2302.13971</a><a href="#fnref6" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn7" role="doc-endnote"><p>Gao et al., The Pile: An 800Gb Dataset of Diverse Text for Language Modeling, 2020. <a href="https://arxiv.org/abs/2101.00027">ArXiV:2101.00027</a><a href="#fnref7" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn8" role="doc-endnote"><p><a href="https://twitter.com/theshawwn/status/1320282149329784833">Shawn Presser on Twitter</a><a href="#fnref8" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn9" role="doc-endnote"><p><a href="https://githubcopilotlitigation.com/">https://githubcopilotlitigation.com/</a><a href="#fnref9" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn10" role="doc-endnote"><p><a href="https://stablediffusionlitigation.com/">https://stablediffusionlitigation.com/</a><a href="#fnref10" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>

</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-07-10</pubDate>
    </item><item>
    <title>[Publication] Evaluating participating methods in image analysis challenges: lessons from MoNuSAC 2020</title>
    <link>https://research.adfoucart.be/publication-disentangled-metrics</link>
    <description><p>It took a long time -- way longer than necessary, to be honest -- but our paper “Evaluating participating methods in image analysis challenges: lessons from MoNuSAC 2020” has finally been published in Pattern Recognition, and is now available online (<a href="https://doi.org/10.1016/j.patcog.2023.109600">doi:10.1016/j.patcog.2023.109600</a>, open access for 50 days using <a href="https://authors.elsevier.com/a/1gxTw77nKkXA3">this link</a>).</p>
<p>It uses the published results of the MoNuSAC 2020 challenge, which include the prediction maps from some of the teams, to study how using complex metrics such as Panoptic Quality lead to poorer insights and hard to interpret results, compared to using separate, independent metrics for each sub-task (here: segmentation, classification and detection). As we demonstrate in the paper, while the ranking based on the PQ doesn’t really tell us much about the capabilities of the different algorithms, using separate metrics give us otherwise invisible insights on the results of the challenge.</p>
<p>The preprint version of the paper can be downloaded from <a href="./preprint/FOUCART22-MoNuSAC.pdf">this website</a>. Only minor modifications were made for the final paywalled version.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-04-14</pubDate>
    </item><item>
    <title>Misusing Hausdorff's Distance</title>
    <link>https://research.adfoucart.be/hausdorff-misuse</link>
    <description><h2 id="hausdorffs-distance-is-useful-but-it-can-be-tricky">Hausdorff’s Distance is useful – but it can be tricky</h2>
<p>I like Hausdorff’s Distance as a segmentation metric. It’s a way to measure the distance between the contours of two objects, and provides useful information that isn’t captured by overal metrics such as the Intersection over Union or the Dice Similarity Coefficient. I talk about it way more extensively in <a href="https://research.adfoucart.be/phd-thesis">my thesis</a>.</p>
<p>What I want to focus on here, however, is how it’s implemented, and particularly how scikit-image’s implementation leads to some mistakes in the way it’s commonly used.</p>
<p>The <a href="https://scikit-image.org/docs/stable/auto_examples/segmentation/plot_hausdorff_distance.html#hausdorff-distance">example from scikit-image’s documentation</a> shows how the metric is computed for two sets of points. This, however, is not how it would generally be used in a segmentation task.</p>
<p>In a segmentation task, we will generally be comparing two binary masks: one with the annotated obect, and one with the prediction, as shown here:</p>

<img src="data:image/png;base64,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//rXu1zrl770pfjKV74SX//61+OFF16I8ePHR0TEnXfeGRERSZLEiSeeGPPnz4+rrroqDj744PjDH/6Q+WZiRw466KCYOXNmnHPOOXHllVfG0KFDIyJi33333eHr3Vk7c/yNGzfGF7/4xfj6178el1xySfz+97+Pa665Jtq1axdXXXXVTj8XQC575ZVXIiJi7733jmXLllX53lxRUREnnHBCPP300zFu3Lg47LDDYsWKFXH11VfHoEGDYtGiRZmzk7/xjW/E3XffHZdeemkMHjw4XnzxxTj55JNj3bp1H1vL448/HsOHD4/evXvH1KlTo3PnzvHaa69lru/yn//5n/Gvf/0rbrnllnjwwQczfba6M7hq2kN//etfx8KFC2PixInRunXrmDJlSpx00knx8ssvR/fu3Xd5jAHSYvPmzVX+P3mSJDU6zvDhw+P666+PRYsWxRFHHJE59s4cJz8/v9YCq5r67W9/G3fffXcsXLgwmjRpUuU+L774YkRE9OvXL2t9s2bNolevXpnt1SkvL48//elPVZ7FtWTJkhg7dmyUlpZGcXFxzJo1K8aMGRMbNmyISy+9NCK2XKPsueeeiy9/+cuxePHi2GuvvWLChAnx1FNPxW9+85safwYlpRLYQ8rKypKISL761a9W2rZp06Zk48aNmaWioiKzbcSIEUlEJHfeeWfWY37wgx8kEZH87Gc/y1p/ww03JBGRPPHEE0mSJMny5cuTiEhmzpxZ6XkjIrn66qszP1999dVJRCRTpkzJ2m/UqFFJYWFhpq7HHnssiYjk5ptvztrvuuuuq3TMqixcuLDamqp7vUmSJF26dElGjBhRaf3AgQOTgQMH1uj424/b8ccfn3z605/eYd0AuWjmzJlJRCR//OMfk40bNybr1q1LHnnkkWTvvfdO2rRpk5SVlVX73vyTn/wkiYjkgQceyFq/9X34tttuS5IkSf73f/83iYjkoosuytpv1qxZSURkvbfPnTs3iYhk7ty5mXU9evRIevTokXzwwQfVvo7vfve7SUQky5cvr7Rt+z6xsz00Sbb0yqKiomTt2rWZdWVlZUl+fn4yefLkausByAVbe8SOli5dumT279KlSzJ06NBqjzdjxowkIpKf/vSnWY/5uOf4uM8Yffr0yXqfr86OPidUZ926dUnXrl2T8ePHZ9W8/evc+llo1apVlY4xZMiQ5N/+7d92+DxnnHFG0rRp02TRokVZ67t06ZLk5eUlS5YsyVo/ePDgpG3btsl7772XWffXv/41adu2bXLiiScmv/3tb5P8/Pzkyiuv3OnXSvqZrkiD0L9//2jWrFlmuemmmyrt86UvfSnr5yeffDJatWoVp5xyStb6rdP5fve73+1yPV/84hezfu7Xr198+OGHsXr16oiImDt3bkREpWu0nH766bv8nNvb/vXWpry8vBg+fHjWun79+sWKFSvq7DkBGrpDDz00mjVrFm3atIlhw4ZFcXFxPPbYY1FUVJTZZ/v35kceeST22muvGD58eGzatCmzHHjggVFcXJyZLlhd3/jyl78cTZvu+MT6ZcuWxauvvhpf//rXo7CwsBZeac176NZrqmxVVFQUHTt21DeARmPrWUzbL1vPxtpZSRVnbP3qV7+q8tjbL9/85jdr6+XUyOWXXx7NmjXb6RkfeXl5NVofEfGd73wnZs2aFd/73veif//+lbb36dMnDjjggKx1p59+eqxduzb+9Kc/ZdZ96lOfijvuuCN+8YtfxLBhw+LII4+sdNkWcpvpiuwxHTp0iBYtWlT5P8SzZ8+O999/P1atWlUpYIqIaNmyZaU7eKxZsyaKi4srvVl27NgxmjZtGmvWrNnlWtu3b5/1c0FBQUREfPDBB5nnbtq0aaX9iouLd/k5t1XV661NLVu2rPRBqaCgoFauDQCQVnfffXf07t07mjZtGkVFRZWmNVT13vzPf/4z3nnnnWqvM/nWW29FRGR60vZ9oqpesr2t1/badtr57qppD62qxoKCgkxfBMh1vXv3ztycZFvt2rWrdA2pHdn6WWjbaxDvv//+Oz1dcU9bsGBB3HbbbfHggw/Ghx9+mPm8UFFREZs2bYp33nknWrRoEQUFBZlesWbNmqwviCK2XAfyk5/8ZJXPMWHChLj22mvjuuuui/PPP7/Kfar6nLV13fY9a+jQoVFUVBT//Oc/4+KLL652eiW5ScjFHtOkSZP4whe+EE888USsWrUq68PD1muGvPbaa1U+tqrUv3379vHcc89FkiRZ21evXh2bNm2KDh06RERkwpztL6S7uyHYpk2bYs2aNVn/419WVrbLx9xWdd9yFBYWVnlx+LfeeivzegHYNdV9gNmqqvfmDh06RPv27eM3v/lNlY/ZevbT1l5RVlYW++yzT2b71l6yI1tvC//3v/99xy+gBna2hwJQe5IkiV/96lfRqlWrrH7To0ePnToz9uqrr97jZyW99NJLkSRJnHTSSZW2rVy5Mj7xiU/E9773vRg7dmx85jOfiYiIF154IeuakJs2bYq//OUvcdppp1U6xoQJE6K0tDRKS0vj29/+drV1VPU5a+u67b+IOe+882LdunXRp0+fuPDCC+PII4+MT3ziEzv3gkk90xXZo8aPHx+bN2+O8847LzZu3Lhbxzr66KPj3XffjV/84hdZ6+++++7M9ogtUyoKCwvj+eefz9rvl7/85S4/9+c///mIiJg1a1bW+tmzZ+/U47c/M2xnde3atdLrWLZsWbz88su1cnwAambYsGGxZs2a2Lx5cwwYMKDS8ulPfzoiInNnw+37xs9+9rOPvbnIv/3bv0WPHj3izjvv3OFdcGvy3r+zPRSA2jNhwoR46aWXYsyYMVmzKhrydMXjjjsu5s6dW2kpKiqKQw89NObOnZuZ+n7IIYdEp06dKt3V/v7774933303Tj755Kz111xzTZSWlsaVV14ZV1999Q7rWLp0afz5z3/OWjd79uxo06ZNHHTQQZl1P/zhD+Pee++N6dOnx8MPPxzvvPNOpbs9ktucycUedfjhh8ett94aF1xwQRx00EHxzW9+M/r06RP5+fmxatWqeOCBByIidmqq3tlnnx233nprjBgxIl577bX4zGc+E88880xMmjQpjj/++DjmmGMiYss372eeeWbceeed0aNHjzjggANiwYIFOx1IVWXIkCFx1FFHxbhx4+K9996LAQMGxB/+8Ie45557durxPXr0iBYtWsSsWbOid+/e0bp16ygpKck6bbkqZ511Vpx55pkxatSo+NKXvhQrVqyIKVOmZL7l393jA1AzX/3qV2PWrFlx/PHHx5gxY+Jzn/tcNGvWLP7+97/H3Llz44QTToiTTjopevfuHWeeeWZMmzYtmjVrFsccc0y8+OKLceONN+5Uz7v11ltj+PDhceihh8ZFF10UnTt3jtdffz0ef/zxTHC29Rv0m2++OUaMGBHNmjWLT3/601nX0tpqZ3soADX3zjvvxB//+MeIiHjvvffi5Zdfjvvuuy+efvrp+PKXvxwTJkzI2n/r+3dNLVq0KDMTZu3atZEkSdx///0REXHwwQdHly5dMvtuXf+3v/0t89jWrVtHRGRdn/Hoo4+OefPm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
" />

Example of synthetic “ground truth” and “predicted” segmentation masks, as well as the overlapping “contours image,” with the pair of points determining Hausdorff’s distance marked in white.


<p>Looking at the hausdorff_distance method in <a href="https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.hausdorff_distance">scikit-image</a>, we can see that it expects as arguments two images. The documentation states that it will compute the Hausdorff distance between nonzero elements of given images, which means that if we want to use it as a segmentation metric, we need to provide as input images where the contours of the objects are nonzero elements.</p>
<p>This, however, is a bit confusing for two reasons. First, most segmentation metrics are computed directly on the segmentation masks, so users of the library who don’t read the documentation closely enough could expect that behaviour to be implemented here. Second, scikit-image provides no method for computing a “contours image” from a segmentation mask. The <a href="https://scikit-image.org/docs/stable/api/skimage.measure.html#skimage.measure.find_contours">find_contours</a> method returns a list of contour points coordinates, not an image. It also uses some interpolation to compute the coordinates, meaning that we get floating points values which can’t be used to directly recreate a “contours image” from the output of find_contours.</p>
<p>The easiest way to get the intended results of the method is to do something like:</p>
<pre class="sourceCode python"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>se = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]])
<a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>gt_contour = ground_truth ^ erosion(ground_truth, se)
<a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>predicted_contour = predicted ^ erosion(predicted, se)
<a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>distance = hausdorff_distance(gt_contour, predicted_contour)</pre>
<p>Where ground_truth and predicted are binary segmentation masks. The contours here are found by eroding the masks and subtracting the eroded mask from the original (thus only leaving the outer layer of pixels, as shown in the previous figure).</p>
<h2 id="does-it-matter">Does it matter?</h2>
<p>The confusion about how to properly compute Hausdorff’s distance is not limited to scikit-image. The 2015 Gland Segmentation challenge, for instance, describes in its post-challenge publication (<a href="https://www.sciencedirect.com/science/article/pii/S1361841516301542?via%3Dihub#eq0008">Sirinukunwattana et al., 2017</a>) the metric as “the most extreme value from all distances between the pairs of nearest pixels on the boundaries of S and G,” but in the mathematical definition <a href="https://warwick.ac.uk/fac/cross_fac/tia/data/glascontest/evaluation/">on the challenge website</a> – and in their MATLAB implementation – actually compute the distances not just on the boundaries, but on all nonzero pixels in the segmentation masks, thus replicating scikit-image’s behaviour.</p>
<p>In code available from recent publications using scikit-image’s implementation such as <a href="https://link.springer.com/chapter/10.1007/978-3-030-98253-9_18">Bourigault et al., MICCAI 21</a> (<a href="https://github.com/EmmanuelleB985/Head-and-Neck-Tumour-Segmentation-and-Prediction-of-Patient-Survival/blob/bb36a0aa953367775d140abcc112342a50066759/src/Segmentation_Task/metrics.py">code</a>) or <a href="https://link.springer.com/chapter/10.1007/978-3-031-16434-7_32">Le Bescond et al., MICCAI 22</a> (<a href="https://github.com/loic-lb/Unsupervised-Nuclei-Segmentation-using-Spatial-Organization-Priors/blob/58200221430f19c955039d7bf56c0c0f9739ef87/performance/metrics.py">code</a>), we see the same implementation, where all pixels of the segmentation mask are considered instead of just the contours.</p>
<p>There can be a significant difference between the two ways of measuring the distance, as illustrated in the synthetic example below:</p>

<img src="data:image/png;base64,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
" />

Example of synthetic “ground truth” and “predicted” segmentation masks, as well as the overlapping “contours image,” with the pair of points determining Hausdorff’s distance marked in white (using the points in the contours only) and black (using all points in the binary masks).


<p>From a quick search on GitHub, I have found <a href="https://github.com/leisuzz/Image-process---segmentation/blob/main/assignment%202.ipynb">other</a> <a href="https://github.com/MPA-ABO/example_repo/blob/a503661a7d0830f4536176afa77b163373659069/evaluate.py#L44">examples</a> of “misuse” of scikit-image’s confusing implementation.</p>
<h2 id="so-whats-next">So what’s next?</h2>
<p>I have raised <a href="https://github.com/scikit-image/scikit-image/issues/6890">an issue</a> on scikit-image’s repository. To avoid backward compatibility issues, I don’t think it’s a good idea to change the behaviour of the method in scikit-image, but I think using a clearer example of how to use it, being more explicit in the docstring and/or add a function to directly compute the distance from the segmentation masks would be interesting improvements.</p>
<p>I’ll see whether the scikit-image community agrees with me (or cares at all about the issues, to start with), and hopefully we can limit the confusion in the future.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-04-13</pubDate>
    </item><item>
    <title>[Opinion] Bing AI: has Microsoft lost its mind?</title>
    <link>https://research.adfoucart.be/opinion-bing-ai</link>
    <description><p>In case you haven’t heard, Microsoft is “<a
href="https://blogs.microsoft.com/blog/2023/02/07/reinventing-search-with-a-new-ai-powered-microsoft-bing-and-edge-your-copilot-for-the-web/">Reinventing
search with a new AI-powered Microsoft Bing and Edge, your copilot for
the web</a>”. Surfing on the ChatGPT craze, they also further announced
the integration of chatbots into <a
href="https://blogs.microsoft.com/blog/2023/02/22/the-new-bing-preview-experience-arrives-on-bing-and-edge-mobile-apps-introducing-bing-now-in-skype/">Skype</a>,
so that you can just send a quick question to your AI pal in the middle
of your conversation with friends and family.</p>
<p>Let’s put aside the fact that it necessarily means giving full access
to the content of your conversations to Microsoft (no end-to-end
encryption if Bing needs to listen in to offer its advice!).</p>
<p>I just want to quickly review the examples that Microsoft has decided
to highlight in its own blog posts to showcase the capabilities of its
new superpowered AI. Because… I’m starting to doubt that anyone even
reads the chatbot outputs before sharing them and saying how
wonderful they are.</p>
<p>Let’s start by the first example, in the Bing AI announcement.</p>
<blockquote>
<p>I am planning a trip for our anniversary in September. What are some
places we can go that are within a 3 hour flight from London
Heatrow?</p>
</blockquote>
<p>Relatively simple and straightforward request. We need destinations
that are:</p>
<ul>
<li>Less than 3 hours flight from London Heatrow.</li>
<li>Suitable / popular for a romantic getaway.</li>
</ul>
<p>If we ask Google for a “romantic trip less than 3 hours flight from
London”, we get several articles with a lot of possible choices, from <a
href="https://www.inspiringtravel.co.uk/other-shores/explore/short-flights-from-uk-to-hot-destinations/">inspiringtravel.co.uk</a>,
<a
href="https://www.thegentlemansjournal.com/getaways-two-hour-flight-from-london/">thegentlemansjournal.com</a>,
or <a
href="https://www.travelrepublic.co.uk/blog/6-beach-holidays-just-3-hours-away">travelrepublic.co.uk</a>.
Not all of those are suitable for the request, but it’s fairly easy to
sift through (almost all include some estimation of the flight time and
what to do there).</p>
<p>What’s Bing AI’s take?</p>

<img src="./img/opinion-bing-ai/city-trip.png"
alt="Bing AI example from Microsoft’s blog" />
Bing AI example from Microsoft’s
blog

<p>First one is Malaga. The provided information seems correct, and it
fits the request, depending on what you consider suitable for a
anniversary trip. So far, so good.</p>
<p>Second one is… Annecy, in France. Certainly a very nice city, but it
doesn’t have an airport, and is therefore absolutely not within a 3 hour
flight from London Heatrow. Ok. One mistake, let’s move on.</p>
<p>Third one is… Florence, Italy. Which as far as I can tell requires a
minimum flight time of four hours.</p>
<p>That’s the first example they chose to highlight: two of the three
proposed destinations do not fit the prompt. In the propositions from
the Google search, you can find Paris, Amsterdam, and plenty of other
destinations which arguably fit the prompt better. Also, you know,
instead of the Venice of France, there is actually a 2h20
minutes flight from London to actual Venice…</p>
<p>Let’s see what <a
href="https://blogs.microsoft.com/blog/2023/02/22/the-new-bing-preview-experience-arrives-on-bing-and-edge-mobile-apps-introducing-bing-now-in-skype/">Bing
AI in Skype</a> can do. Here, they provide three conversation examples.
In the first one, they ask for some vegetarian recipes, and it delivers.
I don’t really see how it’s easier than Google in this case, but fine.
Second one is about cleaning up a full mailbox, and here again the
results seem fine, but you could put the same thing in Google (or
probably even Bing!) and get it in the first result.</p>
<p>The third one is where we finally see a request that requires some
level of “intelligence”: “what should we do during a layover in Spain?
Food an beaches ideally”. That’s a relatively easy question:
Bing AI can choose from all over Spain, and information about beaches
and restaurants are typically not difficult to find. Can you guess how
well it performs?</p>

<img src="./img/opinion-bing-ai/layover.png"
alt="Bing AI in Skype example from Microsoft’s blog" />
Bing AI in Skype example from Microsoft’s
blog

<p>The first suggestion is fine… Almost. At first glance. As
far as I can tell (because the reference is hidden in the screenshot)
La Mallorquina in Barcelona is not a pastry shop, it’s a
textile shop. There is a pastry shop called “La Mallorquina Formentor”,
but it doesn’t seem particularly famous. There is a famous pastry shop
called “La Mallorquina” in Spain… but it’s in Madrid. I’m sure Spanish –
and Catalan – users will be delighted to know Bing AI confuses Madrid
with Barcelona…</p>
<p>The second suggestion is incredible. “If you are in Port of Spain”.
Port of Spain. Which is in Trinidad and Tobago, around
6000km from Spain, is the second option for a layover in Spain from Bing
AI. In the example cherry-picked by Microsoft to showcase how
good their system is. Seriously.</p>
<p>This is absurd. Microsoft may be getting some people to Bing with the
hype, but I really don’t see how they will retain customers if they have
such a high miss rate in their answers.</p>
<p>The fact that a comparatively much smaller mistake made by Bard in
Google’s demo of their competing system apparently had a <a
href="https://www.reuters.com/technology/google-ai-chatbot-bard-offers-inaccurate-information-company-ad-2023-02-08/">huge
impact on their stock</a> while Microsoft’s mistakes (and there are
others <a
href="https://www.theverge.com/2023/2/14/23599007/microsoft-bing-ai-mistakes-demo">in
their other demos</a>) don’t seem to have much impact on the hype either
suggests that tech investors are completely irrational, or that everyone
is so used to Bing being bad that “bad, but with style” is really seen
as an improvement.</p>
<p>Anyway. I can’t wait for this hype cycle to be over so that we can
move beyond large generative models, which seem more and more like a
scientific dead end.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-03-08</pubDate>
    </item><item>
    <title>[Devlog] Getting oriented in anatomical space</title>
    <link>https://research.adfoucart.be/devlog-anatomical-space</link>
    <description><h2 id="ct-images-3d-views-and-anatomical-planes">CT images, 3D views and anatomical planes</h2>
<p>During all my PhD, I’ve managed to avoid dealing with the third dimension. I did some work before that on “structure from motion” for detecting trucks from traffic cameras, but that was ten years ago. Now, however, I have to get away from the confort of two-dimensional images, like the <a href="./devlog-coordinates-scale">digital pathology slides</a> I’m most familiar with, and deal with Computed Tomography, CT in short, images.</p>
<p>The first question when dealing with CT images is… how to “look” at them? You can use some tricks to get a 3D view of the main elements, but if you want to get the “raw” information, the most common way is to use 2D slices cutting in different directions. I have some CT images of mice, which have been injected with a contrasting agent so that the liver is more visible. Let’s take a quick look using the open source <a href="https://www.slicer.org/">3D Slicer</a> software:</p>

<img src="./img/devlog-anatomical-space/Mouse_Full_Layout.png" alt="Different views of a CT Scan. Top-left: transverse plane (i.e. axis are left-right and dorsal-ventral); bottom-left: frontal plane (axis are left-right and anterior-posterior); bottom-right: sagittal plane (axis are dorsal-ventral and anterior-posterior); top-right: 3D visualisation showing the bones and most visible organs" />Different views of a CT Scan. Top-left: transverse plane (i.e. axis are left-right and dorsal-ventral); bottom-left: frontal plane (axis are left-right and anterior-posterior); bottom-right: sagittal plane (axis are dorsal-ventral and anterior-posterior); top-right: 3D visualisation showing the bones and most visible organs

<p>Since on of the goals of my research is to register ex-vivo histology images to these in-vivo CT, it would be very useful to determine a reliable frame of reference for “anatomical coordinates.” The reason that it’s useful (rather than just looking at voxel coordinates in the volume) is that with CT images, it’s very easy to reorient and/or crop the image in different ways to make it more practical to process in one way or another. A coordinate system that is independent from these transforms is quite useful.</p>
<p>The characteristics that I want for this system are therefore:</p>
<ul>
<li>Have an origin point that is reasonably easy to find automatically.</li>
<li>Use axis that are easy to interpret (so, based on the anatomical reference)</li>
<li>Use physical coordinates, i.e. millimeters, rather than pixels.</li>
</ul>
<h2 id="point-and-axis-of-reference">Point and axis of reference</h2>
<p>It seems obvious that the reference point, the origin of our coordinates system, will have to be related to the skeleton. The skeleton is very easy to segment, and the position of the different organs relative to the skeleton can be somewhat predictable. As far as my anatomy knowledge goes, also, it should be present and similar in all subjects…</p>
<p>Looking at the images, I think an interesting candidate is the junction between the most posterior ribs and the vertebral column (so the last <a href="http://www.informatics.jax.org/cookbook/figures/figure32.shtml">floating rib from this diagram</a>). In our example image, we can easily find this spot in the 3D view of the skeleton:</p>

<img src="./img/devlog-anatomical-space/Skeleton_Only.png" alt="3D view of the skeleton. The black arrow points to a possible candidate for an easy to find anatomical reference point" />3D view of the skeleton. The black arrow points to a possible candidate for an easy to find anatomical reference point

<p>So how do we find it automatically?</p>
<p>Let’s start by segmenting the skeleton, using a simple thresholding (it’s fairly easy to find a good threshold automatically in this case, but for simplicity’s sake I’ll hardcode it here). We’ll use SimpleITK to load the volume, but most of the processing will be done with numpy and scikit-image. The useful thing about SimpleITK is that the image object includes information about the “spacing” (the physical size of the voxels).</p>
<pre class="sourceCode python"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>import SimpleITK as sitk
<a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>import numpy as np
<a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>%matplotlib inline
<a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>from matplotlib import pyplot as plt</pre>
<pre class="sourceCode python"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>path_to_volume = r&quot;.\volume.mha&quot;
<a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>volume = sitk.ReadImage(path_to_volume)
<a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>volume_np = sitk.GetArrayFromImage(volume)  # Convert to Numpy array for easier low-level processing
<a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>
<a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>skeleton = volume_np &gt; 100</pre>
<p>In order to view this skeleton, we can “project” the volume on the three different planes:</p>
<pre class="sourceCode python"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>plt.figure(figsize=(20,7))
<a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 1)
<a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>plt.imshow(skeleton.sum(axis=0))
<a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Dorsal &lt;-&gt; Ventral&#39;)
<a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Transverse projection&#39;)
<a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 2)
<a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>plt.imshow(skeleton.sum(axis=1))
<a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Coronal projection&#39;)
<a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 3)
<a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>plt.imshow(skeleton.sum(axis=2))
<a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Ventral &lt;-&gt; Dorsal&#39;)
<a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Sagittal projection&#39;)
<a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_6_0.png" alt="Projection of the skeleton in the three anatomical planes" />Projection of the skeleton in the three anatomical planes

<h2 id="automatically-finding-the-right-place">Automatically finding the right place</h2>
<p>It’s way easier to process 2D images than 3D volumes, so let’s first try to use the coronal projection to find the reference point’s left-right and anterior-posterior coordinates, and we’ll worry about the dorsal-ventral dimension later.</p>
<pre class="sourceCode python"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>coronal_proj = skeleton.sum(axis=1)
<a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>plt.imshow(coronal_proj)
<a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Coronal projection&#39;)
<a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_8_0.png" alt="Coronal projection of the skeleton" />Coronal projection of the skeleton

<p>First, we want to find the path of the vertebral column. Since the body is mostly symmetrical in the left-right dimension, we can try to use the centroid of each row to see if it gives us something interesting:</p>
<pre class="sourceCode python"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>x = np.arange(coronal_proj.shape[1])
<a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>centroids = (coronal_proj * x).sum(axis=1) / coronal_proj.sum(axis=1)
<a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>plt.imshow(coronal_proj)
<a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>plt.plot(centroids, np.arange(coronal_proj.shape[0]), &#39;r-&#39;)
<a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Coronal projection&#39;)
<a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_10_0.png" alt="Coronal projection with “centroid” line" />Coronal projection with “centroid” line

<p>It’s not always well centered because the vertebral column isn’t completely straight, so sometimes just taking the horizontal in this projection is not symmetrical. However, it always remain inside the column, except near the front where the front paws make it more difficult. To adjust for that, we can easily use a “connected components” rule to only keep the largest object in the image:</p>
<pre class="sourceCode python"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>from skimage.measure import label, regionprops
<a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>def select_main_object(binary_array: np.ndarray) -&gt; np.ndarray:
<a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>    &quot;&quot;&quot;Removes any bit that is not connected to the largest object&quot;&quot;&quot;
<a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>    labels = label(binary_array)
<a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>    objs = regionprops(labels)
<a href="#cb6-7" aria-hidden="true" tabindex="-1"></a>    max_area, max_area_idx = 0, -1
<a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>    for obj in objs:
<a href="#cb6-9" aria-hidden="true" tabindex="-1"></a>        if obj.area &gt; max_area:
<a href="#cb6-10" aria-hidden="true" tabindex="-1"></a>            max_area = obj.area
<a href="#cb6-11" aria-hidden="true" tabindex="-1"></a>            max_area_idx = obj.label
<a href="#cb6-12" aria-hidden="true" tabindex="-1"></a>    binary_array[labels != max_area_idx] = 0
<a href="#cb6-13" aria-hidden="true" tabindex="-1"></a>    return binary_array
<a href="#cb6-14" aria-hidden="true" tabindex="-1"></a>
<a href="#cb6-15" aria-hidden="true" tabindex="-1"></a>coronal_proj[select_main_object(coronal_proj &gt; 0) == 0] = 0
<a href="#cb6-16" aria-hidden="true" tabindex="-1"></a>x = np.arange(coronal_proj.shape[1])
<a href="#cb6-17" aria-hidden="true" tabindex="-1"></a>centroids = (coronal_proj * x).sum(axis=1) / coronal_proj.sum(axis=1)
<a href="#cb6-18" aria-hidden="true" tabindex="-1"></a>
<a href="#cb6-19" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb6-20" aria-hidden="true" tabindex="-1"></a>plt.imshow(coronal_proj)
<a href="#cb6-21" aria-hidden="true" tabindex="-1"></a>plt.plot(centroids, np.arange(coronal_proj.shape[0]), &#39;r-&#39;)
<a href="#cb6-22" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb6-23" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb6-24" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Coronal projection&#39;)
<a href="#cb6-25" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_12_0.png" alt="Cleaned-up coronal projection with “centroid” line" />Cleaned-up coronal projection with “centroid” line

<p>That’s better. Now, we can use a simple heuristic to determine the location on the anterior-posterior axis of the junction that interests us. Looking at the values across the “centroid” line, the local minima correspond to the junctions between the vertebrae. We can then check if there are ribs around that junction, and the first junction with ribs starting from the posterior side corresponds to the target.</p>
<pre class="sourceCode python"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>from skimage.feature import peak_local_max
<a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>centerline_x = np.round(centroids).astype(&#39;int&#39;)
<a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>centerline_value = []
<a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>for z, x in enumerate(centerline_x):
<a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>    centerline_value.append(coronal_proj[z, x])
<a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>centerline_value = np.array(centerline_value)
<a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>vertebrae = peak_local_max(centerline_value.max() - centerline_value, min_distance=30)[:, 0]
<a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>
<a href="#cb7-10" aria-hidden="true" tabindex="-1"></a># To find the first vertebra junction with a rib attached, we look at the variance across the left-right line
<a href="#cb7-11" aria-hidden="true" tabindex="-1"></a># passing through the junction
<a href="#cb7-12" aria-hidden="true" tabindex="-1"></a>vertebrae_sorted = np.sort(vertebrae)
<a href="#cb7-13" aria-hidden="true" tabindex="-1"></a>xcs = (np.array([np.arange(coronal_proj.shape[1]) for _ in range(len(vertebrae))]).T - centroids[
<a href="#cb7-14" aria-hidden="true" tabindex="-1"></a>    vertebrae_sorted]).T
<a href="#cb7-15" aria-hidden="true" tabindex="-1"></a>variances = ((xcs ** 2) * coronal_proj[vertebrae_sorted]).sum(axis=1)
<a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>z = 0
<a href="#cb7-17" aria-hidden="true" tabindex="-1"></a>for idx, variance in enumerate(variances):
<a href="#cb7-18" aria-hidden="true" tabindex="-1"></a>    if variance &gt; variances.min()*5:
<a href="#cb7-19" aria-hidden="true" tabindex="-1"></a>        z = vertebrae_sorted[idx]
<a href="#cb7-20" aria-hidden="true" tabindex="-1"></a>        break
<a href="#cb7-21" aria-hidden="true" tabindex="-1"></a>
<a href="#cb7-22" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb7-23" aria-hidden="true" tabindex="-1"></a>plt.imshow(coronal_proj)
<a href="#cb7-24" aria-hidden="true" tabindex="-1"></a>plt.plot(centroids, np.arange(coronal_proj.shape[0]), &#39;r-&#39;)
<a href="#cb7-25" aria-hidden="true" tabindex="-1"></a>plt.plot(centroids[z], z, &#39;wo&#39;)
<a href="#cb7-26" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb7-27" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb7-28" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Coronal projection&#39;)
<a href="#cb7-29" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_14_0.png" alt="Coronal projection with position of the first vertebra junction with a rib attached." />Coronal projection with position of the first vertebra junction with a rib attached.

<p>For the position in the left-right axis, we want to get in the center of the vertebral column, so we have to adjust the value a little bit:</p>
<pre class="sourceCode python"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a># adjust x position
<a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>maxima = peak_local_max(coronal_proj[z], num_peaks=2, min_distance=50)
<a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>x = int(np.round(maxima.mean()))
<a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>
<a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>plt.imshow(coronal_proj)
<a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>plt.plot(centroids, np.arange(coronal_proj.shape[0]), &#39;r-&#39;)
<a href="#cb8-8" aria-hidden="true" tabindex="-1"></a>plt.plot(x, z, &#39;wo&#39;)
<a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb8-10" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb8-11" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Coronal projection&#39;)
<a href="#cb8-12" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_16_0.png" alt="Adjusted position of the junction" />Adjusted position of the junction

<p>We still have to find the dorsal-ventral coordinates. For that, we can go back to the segmented skeleton volume and take the region in the sagittal projection around the anterior-posterior and left-right coordinates that we found:</p>
<pre class="sourceCode python"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>region_of_interest = skeleton[z - 15:z + 15, :, x]
<a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>y = peak_local_max(region_of_interest.sum(axis=0), num_peaks=1)[0, 0]
<a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>
<a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb9-5" aria-hidden="true" tabindex="-1"></a>plt.imshow(region_of_interest, cmap=plt.cm.gray)
<a href="#cb9-6" aria-hidden="true" tabindex="-1"></a>plt.plot(y, 15, &#39;bo&#39;)
<a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Ventral &lt;-&gt; Dorsal&#39;)
<a href="#cb9-8" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb9-9" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Sagittal plane&#39;)
<a href="#cb9-10" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_18_0.png" alt="Slice in the sagittal plane around the reference point." />Slice in the sagittal plane around the reference point.

<p>Now we can check where that point is the three planes of our original volumes:</p>
<pre class="sourceCode python"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>vmin = 0
<a href="#cb10-2" aria-hidden="true" tabindex="-1"></a>vmax = 255
<a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>plt.figure(figsize=(20, 7))
<a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 1)
<a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>plt.imshow(volume_np[z], cmap=plt.cm.gray, vmin=vmin, vmax=vmax)
<a href="#cb10-6" aria-hidden="true" tabindex="-1"></a>plt.plot(x, y, &#39;bo&#39;)
<a href="#cb10-7" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb10-8" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Dorsal &lt;-&gt; Ventral&#39;)
<a href="#cb10-9" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Transverse plane&#39;)
<a href="#cb10-10" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 2)
<a href="#cb10-11" aria-hidden="true" tabindex="-1"></a>plt.imshow(volume_np[:, y, :], cmap=plt.cm.gray, vmin=vmin, vmax=vmax)
<a href="#cb10-12" aria-hidden="true" tabindex="-1"></a>plt.plot(x, z, &#39;bo&#39;)
<a href="#cb10-13" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Left &lt;-&gt; Right&#39;)
<a href="#cb10-14" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb10-15" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Coronal plane&#39;)
<a href="#cb10-16" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 3)
<a href="#cb10-17" aria-hidden="true" tabindex="-1"></a>plt.imshow(volume_np[:, :, x], cmap=plt.cm.gray, vmin=vmin, vmax=vmax)
<a href="#cb10-18" aria-hidden="true" tabindex="-1"></a>plt.plot(y, z, &#39;bo&#39;)
<a href="#cb10-19" aria-hidden="true" tabindex="-1"></a>plt.xlabel(&#39;Dorsal &lt;-&gt; Ventral&#39;)
<a href="#cb10-20" aria-hidden="true" tabindex="-1"></a>plt.ylabel(&#39;Anterior (head) &lt;-&gt; Posterior (tail)&#39;)
<a href="#cb10-21" aria-hidden="true" tabindex="-1"></a>plt.title(&#39;Sagittal plane&#39;)
<a href="#cb10-22" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-anatomical-space/output_20_0.png" alt="CT volume in the three anatomical planes with the automatically found reference point" />CT volume in the three anatomical planes with the automatically found reference point

<p>That looks decent enough. With that, we can therefore determine the “origin” point of our system of reference. With the spacing of the volume, we can also compute coordinates in physical spaces along the three anatomical axis:</p>
<pre class="sourceCode python"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>print(volume.GetSpacing())
<a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>spacing = np.array(volume.GetSpacing())
<a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>origin = np.array((x, y, z))  # left-right, dorsal-ventral, anterior-posterior
<a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>coords_px = np.array((400, 300, 150))
<a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>coords_phys = (coords_px-origin)*spacing
<a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>print(f&quot;Coordinates {coords_px}px correspond to a point that is \n{coords_phys[0]:.2f}mm right,\n{coords_phys[1]:.2f}mm ventral, and\n{coords_phys[2]:.2f}mm anterior\nfrom the origin at {origin}px, or a distance of {np.sqrt((coords_phys**2).sum()):.2f}mm&quot;)</pre>
<pre>(0.040892000000000005, 0.040892000000000005, 0.040892000000000005)
Coordinates [400 300 150]px correspond to a point that is 
4.50mm right,
-7.93mm ventral, and
-4.21mm anterior
from the origin at [290 494 253]px, or a distance of 10.05mm</pre>
<p>Consolidating all these methods into proper functions and classes, we can check that the algorithms works on different images:</p>
<pre class="sourceCode python"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a>from registration.ct import find_anatomical_reference
<a href="#cb13-2" aria-hidden="true" tabindex="-1"></a>from segmentation.ct import skeleton_from_volume
<a href="#cb13-3" aria-hidden="true" tabindex="-1"></a>from imagetools.vectors import zyx2vector
<a href="#cb13-4" aria-hidden="true" tabindex="-1"></a>from viz.plot import plot_volume_slices
<a href="#cb13-5" aria-hidden="true" tabindex="-1"></a>import SimpleITK as sitk
<a href="#cb13-6" aria-hidden="true" tabindex="-1"></a>
<a href="#cb13-7" aria-hidden="true" tabindex="-1"></a>paths = [r&quot;E:\data\PTL\PTL01\Round C1\PTL01_20221220_in vivo\4h\PTL01_20221220_09_1C1G_4H_Rec\volume.mha&quot;,
<a href="#cb13-8" aria-hidden="true" tabindex="-1"></a>         r&quot;E:\data\PTL\PTL01\Round C1\PTL01_20221220_in vivo\4h\PTL01_20221220_10_1C1D_4H_Rec\volume.mha&quot;,
<a href="#cb13-9" aria-hidden="true" tabindex="-1"></a>         r&quot;E:\data\PTL\PTL01\Round C1\PTL01_20221220_in vivo\4h\PTL01_20221220_12_1C1G1D_4H_Rec\volume.mha&quot;]
<a href="#cb13-10" aria-hidden="true" tabindex="-1"></a>
<a href="#cb13-11" aria-hidden="true" tabindex="-1"></a>for p in paths:
<a href="#cb13-12" aria-hidden="true" tabindex="-1"></a>    volume = sitk.ReadImage(p)
<a href="#cb13-13" aria-hidden="true" tabindex="-1"></a>    volume_np = sitk.GetArrayFromImage(volume)
<a href="#cb13-14" aria-hidden="true" tabindex="-1"></a>    skeleton = skeleton_from_volume(volume_np, acc_max=1e-5, area_min=1e3)
<a href="#cb13-15" aria-hidden="true" tabindex="-1"></a>    origin = find_anatomical_reference(skeleton, zyx2vector(volume.GetSpacing()))
<a href="#cb13-16" aria-hidden="true" tabindex="-1"></a>    plot_volume_slices(volume_np, point=origin, figsize=(20,7))</pre>

<img src="./img/devlog-anatomical-space/output_24_0.png" alt="CT volume in the three anatomical planes with the automatically found reference point (first image)" />CT volume in the three anatomical planes with the automatically found reference point (first image)


<img src="./img/devlog-anatomical-space/output_24_1.png" alt="CT volume in the three anatomical planes with the automatically found reference point (second image)" />CT volume in the three anatomical planes with the automatically found reference point (second image)


<img src="./img/devlog-anatomical-space/output_24_2.png" alt="CT volume in the three anatomical planes with the automatically found reference point (third image)" />CT volume in the three anatomical planes with the automatically found reference point (third image)

</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-01-24</pubDate>
    </item><item>
    <title>Can ChatGPT write an academic paper?</title>
    <link>https://research.adfoucart.be/review-chatgpt-paper</link>
    <description><h1 id="review-of-a-day-in-the-life-of-chatgpt">Review of “A Day in the Life of ChatGPT”</h1>
<p>Mashrin Srivastava tried an interesting ChatGPT experiment: a “paper completely written by ChatGPT” (originally published on LinkedIn, now available as a preprint on <a href="https://doi.org/10.13140/RG.2.2.34470.60480">ResearchGate</a>).</p>
<p>He appended to the generated paper the whole sequence of prompts and responses that was used to generate it. The original prompt was for an article to submit to a workshop on Sparsity in Neural Networks at ICLR 2023.</p>

<img src="./img/chatgpt-paper/title.jpg" alt="Title of the generated article (“A Day in the Life of ChatGPT as a researcher:”) was added by Mashrin, the rest comes from the bot" />Title of the generated article (“A Day in the Life of ChatGPT as a researcher:”) was added by Mashrin, the rest comes from the bot

<p>I don’t think it makes a lot of sense to review the resulting paper like I would do for a regular paper (I can’t pretend that I don’t know it’s a generated text), but I think the exercise is really interesting, so I want to take a deep dive into it.</p>
<p>A few disclaimers before I start:</p>
<ul>
<li>To be honest about my own biases, I have been quite open about the fact that I don’t think ChatGPT is revolutionary or quite as useful as the hype around it make it look, and that I’m very skeptical of the actual value of Large Language Models as anything else than fun text generators. But I’m sometimes wrong about things (allegedly), so who knows?</li>
<li>I’m not an expert in sparsity. I’m enough of an expert in machine learning to know why it’s an interesting topic in neural networks, and to know the basics of how to obtain it (e.g. regularization techniques), but I can’t fully judge the scientific quality of a novel paper on the topic. So in terms of accuracy I’ll focus on what I can easily check.</li>
</ul>
<p>Okay, let’s get started !</p>
<h2 id="looking-at-the-prompts-sequence">1. Looking at the prompts sequence</h2>
<p>I started from the Appendix, so I could follow along the generation of the paper to better get a sense of where the ideas came from and how much “prompting” it actually needed. It’s really great from Mashrin to include all of it, as it makes the analysis way more complete. Thanks!</p>
<h3 id="generating-an-outline">Generating an outline</h3>
<p>The first prompt asked for a paper idea for the workshop, giving to ChatGPT the workshop description.</p>
<p>Here, ChatGPT mostly takes point by point all the proposed topics from the workshop description, and arranges them into sections. This is fairly typical from what I’ve seen of ChatGPT: it tends to really want to include everything you mention in its answers.</p>

<img src="./img/chatgpt-paper/workshop-v-outline.png" alt="Workshop description (left) in the prompt vs proposed outline (right) by ChatGPT" />Workshop description (left) in the prompt vs proposed outline (right) by ChatGPT

<p>The result here is that the outline proposes a wide but superficial survey paper, which would not really be fitting for a workshop. The structure reads more like the chapters of a book than the sections of a conference paper.</p>
<p>So: not a great start, but nothing particularly bad right now.</p>
<h3 id="generating-the-introduction">Generating the introduction</h3>
<p>Large portions of the introduction are again directly taken from the workshop description. It then rewrites the paper outline, which itself also came from the prompt. Having the outline explained in the intro is normal, but this means that right now we still don’t have any added value from ChatGPT: if we want to know what are interesting topics related to sparsity, we can read the workshop description and we’ll have as much info as with the current intro.</p>

<img src="./img/chatgpt-paper/intro.jpg" alt="Introduction generated by ChatGPT" />Introduction generated by ChatGPT

<p>Also, at this point we have zero citations, including for assertions of facts such as:</p>
<ul>
<li>The range of applications of DNN success (medical diagnostics, autonomous driving)… There should at least be some citation to review papers in those domains.</li>
<li>The large carbon footprint and e-waste.</li>
<li>The potiental of a “sparsity” approach.</li>
</ul>
<h3 id="generating-the-background-section">Generating the “background” section</h3>
<p>The first problem here – caught by Mashrin – is that there is still zero citations, which becomes very problematic in what is typically a citation-heavy part of a paper.</p>
<p>The provided definitions are then very surface-level, and arguably wrong: for instance, “weight decay” is not by itself a pruning technique. It’s a regularization technique which can be used in conjunction with a pruning method which removes connections with weights that are close to zero.</p>
<p>In general, the level of the explanations at this point could be acceptable for a student assignment (assuming it’s correct, I don’t see many obvious errors but there could be), not quite for an actual research paper.</p>
<p>Mashrin then asks ChatGPT to provide citations and to add more details “for a research paper.” We’re moving away a bit from the “completely generated by ChatGPT” scenario, but that’s a bit nitpicky so let’s keep going.</p>
<p>The response doesn’t have more details, but did add citations and a reference list, so let’s go through that.</p>

<img src="./img/chatgpt-paper/background-start.jpg" alt="Start of the background section generated by ChatGPT" />Start of the background section generated by ChatGPT

<p>The first reference is “Sparsity refers to the explicit or implicit reduction of the number of non-zero parameters in a neural network (LeCun et al., 1990).” LeCun et al. is not in the reference list (which I think just got truncated due to limits on the output size). Much later in the process, Mashrin asks ChatGPT to provide a BibTeX citation for it, to which we get a reply mentioning a paper called “Optimal Brain Damage” by LeCun, Denker, Solla, Howard and Jackel. This is not exactly correct, as that paper is from 1989 and with only LeCun, Denker, and Solla. While the paper does talk about pruning and reducing the size of a network, it does not provide such a definition of sparsity.</p>
<p>So let’s check some other references:</p>
<ul>
<li>Sparsity can be introduced in various ways, including through the use of pruning, quantization, and low-rank decomposition (Han et al., 2015): the reference is correct, but the paper really only deal with pruning (although it does mention quantization and low-rank approximation in its related works).</li>
<li>Pruning involves the removal of individual parameters or connections in a neural network based on their importance or relevance to the model’s performance (Hassibi et al., 1993). Pruning can be performed either manually or through the use of automated algorithms.: the reference provided has the wrong year (it’s 1992), but it exists. The paper talks about pruning, but it doesn’t really state such a definition, so it’s not ideal. Also, “manual” pruning of a deep neural network doesn’t sound like a very realistic prospect…</li>
<li>One common automated pruning method is weight decay, which adds a regularization term to the objective function that encourages the weights to be small (Krogh &amp; Hertz, 1992): The reference got truncated as well, but it’s in the final paper, so let’s use the info from there. The paper does explain weight decay, but does not state that it is a pruning method (it’s explored as a way to combat noise).</li>
</ul>
<p>Let’s stop here for a moment: the citations are generally “on topic,” but they are clearly not the source of the information.</p>
<p>Credit where it’s due, however: ChatGPT shows itself here to be a decent source of papers to look at to actually get to know the topic… at least from a fairly general perspective (up to now at least this is all very superficial).</p>
<h3 id="generating-the-sparse-training-algorithms-section">Generating the “sparse training algorithms” section</h3>
<p>Still looking at the citations, here our first is to Krogh and Hertz, 1992, but in the references the title corresponds to another paper from the same authors (from 1991). Again, the citation is “on topic,” but does not explicitly provide the assertion cited.</p>

<img src="./img/chatgpt-paper/algo-start.jpg" alt="Start of the sparse training algorithms section generated by ChatGPT" />Start of the sparse training algorithms section generated by ChatGPT

<p>We also here see repetitions from things already written in the previous chapter. This section barely brings any new details or new pieces of information to the table. The algorithms provided are:</p>
<ul>
<li>Weight decays methods: this is indeed a standard way of inducing sparsity. The “Lasso / Ridge” nicknames are only used in regression models, and it doesn’t apply to the “regularization term” (see e.g. <a href="https://towardsdatascience.com/l1-and-l2-regularization-methods-ce25e7fc831c">here</a> for more), so that part is not correct. Last sentence is correct, but the citation is incorrect. Van der maaten and Hinton is from 2008, not 2010, and the title is “Visualizing data using t-SNE,” not “Getting the most out of a neural network by minimizing the amount of parameter” (which doesn’t exist as far as I can tell). Also, it has nothing to do with weight decay.</li>
<li>Gradient-based pruning methods: the explanation is mostly incorrect (the “standard approach” would rather be to sort the gradients and remove the smallest ones rather than using a hard threshold), and neither of the cited papers (Han et al., van der Maaten et al.) talk about it.</li>
</ul>
<p>Then it does some improv on the limitations of those two “methods” (with again van der Maaten cited for no reason).</p>
<p>Mashrin prompts it again for “more sparsity algorithms.” We get:</p>
<ul>
<li>Structured pruning, pointing again to Han et al. Han et al. does not mention anything called structured pruning. It does not mention pruning an entire layer either. In fact, the definition of structured pruning here is just incorrect.</li>
<li>Sparse initialization. The provided source is not about sparse initialization, but about ReLU activation functions.</li>
<li>Column sampling. There is no “column sampling” algorithm as far as I can find. The reference does present a method to reduce parameters, but not by the described method.</li>
<li>Binary weights. This method does not reduce the amount of parameters or induce sparsity. Also, it’s badly described: the weights are only restricted during the forward and backward passes of the training, but they are still stored as usual. It would also not necessarily require “specialized hardware.”</li>
<li>Low-precision weights. That’s just quantization again, which was previously mentioned in the paper.</li>
</ul>
<p>Mashrin asks for more again, and we get “Dynamic sparsity” (the text doesn’t really correspond to what the paper describes); “Structural sparsity,” which cites a paper by Gao et al. that doesn’t exist, and provides an incorrect definition; and “Functional sparsity,” which cites an article with the wrong authors and year (and which doesn’t correspond to the explanation, which is kind of nonsensical as “constraining the activation to follow a specific function” doesn’t mean anything: that’s just what activation functions do in general).</p>
<p>So, to summarize: rehashing things that superficially develop what was in the prompt, with “algorithms” that and are not particularly well explained, are often badly attributed, are badly named so that it’s hard to find more about them, or just don’t exist.</p>
<h3 id="moving-on-to-novel-ideas">Moving on… to “novel” ideas</h3>
<p>I’m not going to be as detailed for the rest, as it would get very repetitive very quickly, but in the next section on Hardware the first citation is already wrongly attributed, and we keep the same pattern: superficial explanations that “sound right” but are often imprecise, incomplete or just wrong, with citations that don’t match (when they exist).</p>
<p>In the “compression” section, we see the same techniques that were already presented before (pruning, quantization), so no new information.</p>
<p>At some point, Mashrin asks for a novel idea for future research in the area of compression for large-scale neural networks. This moves again away from the “all-written-by-ChatGPT” concept, but it’s interesting. Lack of novelty is often seen as one of the main limitations of LLMs. So what can ChatGPT come up with?</p>

<img src="./img/chatgpt-paper/novel-idea.jpg" alt="A novel idea on compression proposed by ChatGPT" />A novel idea on compression proposed by ChatGPT

<p>Well, we see a reformulation of ideas that were already presented before (“adaptively adjust the level of sparsity in the network based on the specific characteristics of the input data,” which nearly matches what Dynamic Sparsity was presented as). The proposed method also doesn’t really make sense. “Jointly optimizes the network weights and the sparsity pattern of the network based on the input data?” … yes? The “sparsity pattern” is directly linked to the “network weights” (connections that can be pruned are connections with weights close to zero), and the training is obviously based on the training data, so what does that even mean? The rest of the explanation similarly makes no sense (but it “reads” nicely!)</p>
<p>ChatGPT is prompted for more novel ideas.</p>
<p>We get… really rewording of the same, or of previously explained concepts, or so vague as to be completely unusable.</p>
<p>Anyway, I think we get the point. The conclusion is also mostly empty of actual content. It’s basically all “sparsity would be more efficient, so it’s nice, but there are tradeoffs, and it’s difficult.”</p>
<p>Finally, Mashrin asks for an abstract and bibtex citations of the references. The references, as we’ve seen, are mostly accurate, but sometimes made up, which is exactly what you want from an academic paper!</p>
<h2 id="taking-a-step-back">2. Taking a step back</h2>
<p>So all of these results from the prompts were then compiled into a full paper. At least one citation has been changed in the full paper from the prompts results (LeCun, 1990 has become Liu, 2015, I haven’t gone throuhg all), but otherwise it’s just some reformatting.</p>
<p>What can we make of all of this? Here are my main thoughts:</p>
<h3 id="chatgpt-didnt-understand-the-assignment">ChatGPT didn’t understand the assignment</h3>
<p>That’s really important to note again, I think, because it shows a key problem with LLMs – they don’t have, nor understand, intent. Well, they don’t understand anything, but let’s move on from that.</p>
<p>The original prompt was to “suggest a paper for the below conference workshop on Sparsity in Neural Networks.” This is not the kind of venue where you try to write a global review of the whole domain discussed in the workshop. Such workshop are typically for really novel – often work-in-progress – ideas that can move the field forward in one or a few of the specific topics ([just look at the <a href="https://sites.google.com/view/sparsity-workshop-2022/accepted-papers?authuser=0">list of paper from the 2022 edition</a>). Everything that is in this ChatGPT paper would (hopefully) be considered basic, common knowledge for attendees of such a workshop.</p>
<p>I mean, the parts that are correct.</p>
<h3 id="its-really-just-a-excellent-word-generator">It’s really just a (excellent) word generator</h3>
<p>Some people seem to get really upset when they are reminded that LLMs are “stochastic parrots,” but this paper is a great example of how true that is.</p>
<p>It’s really just a word generator.</p>
<p>The structure of the “proposed paper” is taken directly from the prompt. For all the “detailed” section, it’s a lot of superficial, sometimes nonsensical paragraphs that are “on topic” but don’t bring anything new to the table.</p>
<p>It doesn’t even really work as a “quick review,” which would be fine for personal usage if not for publication, because too much of it is just empty of content or wrong. The citations are sometimes relevant, but most of it is way too old to be really good starting points to get the state-of-the-art.</p>
<h3 id="can-chatgpt-produce-valuable-science">Can ChatGPT produce valuable science?</h3>
<p>From what I can see here: no. I can’t say exactly how I would feel seeing this as a reviewer without knowing that it was a ChatGPT-generated text, or without going through the prompt, but it would certainly be a hard reject from me.</p>
<p>Besides the fact that it’s not appropriate for the workshop, there are too many obvious red flags. The superficiality, the repetitions, the citations that don’t match the text… These are all things that I notice when I review papers or grade student work, and there are so many of them here that there is no chance that I would let it pass.</p>
<p>I’m not enough of a specialist in questions of sparsity techniques and efficiency in neural networks, so I can’t really judge how wrong it is… but that’s even more of a red flag: even as a non-specialist, it’s very obviously wrong in many places.</p>
<h3 id="can-chatgpt-be-useful-as-a-study-pal">Can ChatGPT be useful as a study pal?</h3>
<p>If it’s a topic you don’t actually care about and you just want to have a superficial level of knowledge to get the bare minimum to pass a class, sure.</p>
<p>Otherwise I would avoid.</p>
<h2 id="conclusions">3. Conclusions</h2>
<p>I was skeptical before reading the paper, but I tried to keep an open mind (believe it or not!).</p>
<p>I must say I’m absolutely impressed by the quality of the text. It reads like a scientific paper. I totally understand why many people see this as amazing.</p>
<p>But in the end… Stochastic parrot remains the best description there is. As soon as you go beyond the superficial level of “reading the text,” and you try to parse its meaning and verify what it says, it breaks down into noise.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2023-01-07</pubDate>
    </item><item>
    <title>[Opinion] ChatGPT vs Wolfram Alpha</title>
    <link>https://research.adfoucart.be/chatgpt-v-wolfram</link>
    <description><p>This post was originally published on <a
href="https://www.linkedin.com/posts/adrien-foucart-57055041_wolframalpha-making-the-worlds-knowledge-activity-7014296245619826688-DE9q?utm_source=share&amp;utm_medium=member_desktop">LinkedIn</a>.</p>
<p>A big misconception I see with ChatGPT relates to the way it “knows”
data. I’ve seen many statements like “ChatGPT has a knowledge base that
ends in 2021”, or that it “cannot access Google to check its answers”,
presented as limitations that may be addressed in future versions.
Surely, if it “knows” stuff until 2021, it cannot be that hard to update
its knowledge base with more up to date information, or even real-time
information with a Google-like web scraper in the background, right?</p>
<p>The problem is that ChatGPT doesn’t “know” data until 2021, not in
the way that is implied by the idea of “updating” it. If you ask about
the birth date of Napoleon, ChatGPT doesn’t have some biographical data
that it can query to retrieve the correct information. What it may have
is a strong association between the words “Napoleon”, “birth date” and
“August 15, 1769”, if they were often close together in its training
data, which is very likely. It will also “know” from its training data
that a question about a birth date is very likely to require an answer
in a date-like format. Putting all of that together, it will probably
give the right answer most of the time.</p>
<p>But this is not knowledge from a database: the information is
embedded in the parameters of the network. That means that “updating”
the information, or adding new information, requires retraining
the model. Which is, a), very expensive to do, and b), may interfere
with some other previously held “information”.</p>
<p>So a ChatGPT-like model will never be able to function as an
up-to-date source of information, because you can’t just retrain it
regularly with new information scraped from news sources, or Wikipedia,
or wherever. Likewise, it cannot just be plugged into a search engine to
get the information on-the-fly.</p>
<p>A closer thing to this use case, an actual knowledge base that can be
queried using natural language, actually already exists (with its own
important limitations): it’s Wolfram Alpha.</p>
<p>If you ask Wolfram Alpha “<a
href="https://www.wolframalpha.com/input?i=what+is+the+birthdate+of+Napoleon">what
is the birthdate of Napoleon</a>”, it will parse the query and,
crucially, start by giving you information about how it interpreted it,
and the assumptions it made. For instance: that you are referring to
“Napoleon” the royal person and not the fictional character from Animal
Farm. Then it will give you the result, and then dump some additional
related biographical information. This is super important, because it’s
a lot easier to determine if the information can be trusted or not.</p>
<p>Is Wolfram Alpha a perfect source of information? Certainly not. It’s
not as good at parsing natural language, its answers are not formatted
in a nice prose, and it doesn’t attempt to do things like write computer
code or poetry. But at least, if it gives you an answer… it’s probably
correct. Which in my humble opinion is fairly important if you want to
use something as a knowledge source…</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-12-29</pubDate>
    </item><item>
    <title>[Publication] Review of digital pathology segmentation challenges</title>
    <link>https://research.adfoucart.be/publication-review-segmentation-challenges</link>
    <description><p>Our paper “Shortcomings and areas for improvement in digital pathology image segmentation challenges” is <a href="https://authors.elsevier.com/a/1gFNO3BessoALa">now available</a> in its final version in the journal Computerized Medical Imaging and Graphics (doi: <a href="https://doi.org/10.1016/j.compmedimag.2022.102155">10.1016/j.compmedimag.2022.102155</a>).</p>
<p>It’s probably the most important of the articles written during my thesis. We compiled information about 21 digital pathology challenges organized betwen 2010 and 2022 that included a segmentation task, and analyzed different aspects:</p>
<ul>
<li>How the task definitions have evolved over the year, from simple binary or instance segmentation to semantic segmentation and instance segmentation and clasisification.</li>
<li>How the reference annotations were generated: despite the high level of inter-expert disagreement exhibited by most digital pathology tasks, many challenges only rely on a single expert to generate their annotations.</li>
<li>Their evaluation processes. This is in my opinion the most important part of the study, where we identify a serious lack of transparency in the exact steps of the evaluation process, and show that beside the choice of “metric” (which for segmentation tasks is generally either the IoU, the DSC or Hausdorff’s Distance), there are many choices to be made in the evaluation pipeline (rules for matching instances, aggregation process…) which are often less precisely described, despite the significant impact that they can have on the results. We also note that few challenges make their full evaluation code public.</li>
<li>How top-ranked methods have changed over the years, with increasingly complex deep learning architectures (and often ensemble of networks), embedded in a relatively settled pipeline: some form of stain normalization, patch extraction, data augmentation…</li>
</ul>
<p>We take in particular a closer look at three selected challenges with some unique or less common characteristics: GlaS 2015 (which included several subtasks ranked separately), Gleason 2019 (which publicly released individual annotations from multiple experts), and MoNuSAC 2020 (which publicly released their full test set annotations, their evaluation code, and several of the participating teams’ prediction maps).</p>
<p>Segmentation challenges are extremely difficult to organise and properly evaluate. In order to make the “competitive” aspects of the challenge work, we often have to make choices which may make the results less reliable in terms of the scientific perspective they offer, such as ignoring inter-expert disagreement and using metrics which are not necessarily well aligned with the clinical or biological task. To ensure that we fully leverage the time and effort taken to organise those challenges, we highlight the need for more transparency in the process:</p>
<blockquote>
<p>Restricting access to the datasets (and the test dataset in particular) is obviously necessary while the challenge is underway, but becomes a barrier to subsequent research once the challenge is over. Challenge websites are also often abandoned once the final ranking has been published and/or the related conference event is over. Even though all the reviewed challenges are relatively recent, a lot of the information has been lost, or has become very difficult to find, with some websites no longer available, some links to the datasets no longer working, and contact email addresses not responding as organisers have moved on to other projects.</p>
</blockquote>
<p>We emphasize that, in order for challenge results to be fully reproducible (and therefore to be able to compare them to new results), it is necessary to have access “to the evaluation code, the participants’ predictions and the full dataset, including the test set annotations.”</p>
<p>The preprint (which is only very slightly different from the final version) will remain available <a href="./preprint/FOUCART22-Challenges.pdf">here on this website</a>.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-12-14</pubDate>
    </item><item>
    <title>[Devlog] Supervoxel CT liver segmentation</title>
    <link>https://research.adfoucart.be/devlog-slic-liver-segmentation</link>
    <description><p>I have some micro-CT images of mice, where I need to segment the liver. I haven’t done a lot of 3D image processing before, and after spending a few years in the “deep learning” side of the force, I thought it would be nice to look at some good classic image processing methods and to see how I could use them. The images are not too hard, because a contrast product was injected in the mice before the acquisition, so that the liver is a bit brighter than other soft tissues in the images.</p>

<img src="./img/devlog-slic-liver-segmentation/PTL01-slices-liver.png" alt="Three slices from the micro-CT volume, where liver tissue is lighter than the other soft tissues" />Three slices from the micro-CT volume, where liver tissue is lighter than the other soft tissues

<p>It’s not quite as easy as doing a binary thresholding, however. If we look at the histogram of the whole volume with values scaled between 0 and 255, we seem to have four main distributions, which mostly correspond to background, soft tissue, contrasted soft tissue, and bones. The distributions, however, are very much overlapping. So if we try to just take “natural” thresholds manually, like here 130 and 200, we end up taking a lot of bones and missing some actual liver tissue. Not ideal.</p>

<img src="./img/devlog-slic-liver-segmentation/liver-4h-histogram.png" alt="Histogram of the micro-CT values." />Histogram of the micro-CT values.


<img src="./img/devlog-slic-liver-segmentation/seg3d-threshold.png" alt="Result of 3D segmentation, taking every voxels with a value between 130 and 200. Shown using 3D Slicer." />Result of 3D segmentation, taking every voxels with a value between 130 and 200. Shown using <a href="https://www.slicer.org/">3D Slicer</a>.

<p>A method that I’ve always liked in 2D are “superpixels,” where you start by making a regular grid that is deformed so that it sticks to the borders in the image, creating large irregular but mostly homogeneous “pixels.” What’s interesting when you do that is that your “new” image is a lot “smaller” (so easier to process) while keeping the most useful information (where the borders are) intact. In 3D, we don’t have pixels, we have voxels, but the process can still apply. And reducing the size of the image in a clever way like “supervoxels” seems like it could be really useful in 3D images, as the processing time can quickly become very large. This is not a particularly original idea: I quickly checked, and found a paper from 2016 that basically had most of the pipeline that I had in mind (<a href="#ref-wu2016" role="doc-biblioref">Wu et al. 2016</a>) for the same application (in humans, though). I’m going to simplify things a little bit however, as the presence of the contrast product makes it a little bit easier to find the region of interest.</p>

<img src="./img/devlog-slic-liver-segmentation/wu2016.png" alt="Results obtained in (Wu et al. 2016) (slice and 3D reconstruction)" />Results obtained in (<a href="#ref-wu2016" role="doc-biblioref">Wu et al. 2016</a>) (slice and 3D reconstruction)

<p>The pipeline I tested is as follows:</p>
<ul>
<li>Compute supervoxels with SLIC</li>
<li>Create graph connecting neighbours, and compute basic stats per supervoxel (mean intensity, volume)</li>
<li>Filter out supervoxels outside of expected range of mean intensity values for the liver</li>
<li>Iteratively merge neighbours of similar intensities.</li>
<li>Select largest merged supervoxel as “liver” segment.</li>
</ul>
<p>Let’s have a look at what we can get.</p>
<h3 id="slic-supervoxels">SLIC supervoxels</h3>

<img src="./img/devlog-slic-liver-segmentation/slic_supervoxels.png" alt="SLIC supervoxel labels for a slice." />SLIC supervoxel labels for a slice.

<p>SLIC is a commonly used algorithm for finding superpixels (or, in 3D, supervoxels) (<a href="#ref-achanta2012" role="doc-biblioref">Achanta et al. 2012</a>). I use here the <a href="https://simpleitk.org/doxygen/v1_2/html/namespaceitk_1_1simple.html#a98f7004b11762df8123df920781087c1">implementation</a> from the SimpleITK library, which is relatively straightforward:</p>
<pre class="sourceCode python"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>slic = sitk.SLIC(volume, 
<a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>                 superGridSize=(grid_size, grid_size, grid_size),
<a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>                 spatialProximityWeight=proximity_weight)</pre>
<p>It has two main parameters to play with: the size of the “super grid,” which will determine how many superpixels there are, and the “spatial proximity weight,” which will determine whether the resulting grid will be “more regular” or if you allow more deformations. Here, I have set the grid size to 20x20x20, and the proximity weight to 20.0. As we can see in the figure, in homogeneous regions (like in the background), the supervoxels tend to be very regular, while in regions with more borders they deform to adapt to the local information.</p>
<h3 id="computing-the-graph">Computing the graph</h3>
<p>After the previous step, we have a 3D volume with, for each voxel, a label so that every voxel that has the same lable belongs to the same supervoxel. We want to have a “graph” of how those supervoxels are connected together, so we need to find their neighbours. The simplest way of doing that is to iterate through the supervoxels, dilate them, and find all the other supervoxels that intersect with the dilated volume:</p>
<pre class="sourceCode python"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a># note: slic here has been converted to a numpy array
<a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>idxs = np.unique(slic)
<a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>for idx in idxs:
<a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>    mask = volume == idx
<a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>    neighbours = list(np.unique(slic[dilation(mask, el)]))
<a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>    neighbours.remove(idx) # remove current supervoxel from list of neighbours</pre>
<p>And we can at the same time compute the average intensity value of the region in the volume, and save all that meta-information in a dedicated structure. The problem with this simple approach is that it takes a lot of time: for each supervoxel, we need to make a binary comparison and a morphological operation on the entire volume. And in 3D, things scale up very fast. Without the morphological operation, running this code would already take about 5-6h on my computer. To speed things up, we go through the volume by more reasonable chunks of around 20x20x20 voxels, updating the supervoxel information and the neighbours lists as we go along:</p>
<pre class="sourceCode python"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>for z in range(0, volume.size.z, window_size):
<a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>    for y in range(0, volume.size.y, window_size):
<a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>        for x in range(0, volume.size.x, window_size):
<a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>            chunk_slic = slic[z:z + window_size, y:y + window_size, x:x + window_size]
<a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>            chunk_volume = volume_array[z:z + window_size, y:y + window_size, x:x + window_size]
<a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>            idxs = np.unique(chunk_slic)
<a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>            for idx in idxs:
<a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>                if idx not in supervoxels:
<a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>                    supervoxels[idx] = Supervoxel(idx=idx, total=0, volume=0)
<a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>
<a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>                mask = chunk_slic == idx
<a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>                neighbours = list(np.unique(chunk_slic[dilation(mask, el)]))
<a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>                supervoxels[idx].add(total=chunk_volume[mask].sum(),
<a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>                                     volume=np.count_nonzero(mask),
<a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>                                     neighbours=neighbours)
<a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>                supervoxels[idx].add_chunk(IntVector(x=x, y=y, z=z),
<a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>                                           IntVector(x=x+window_size, y=y+window_size, z=z+window_size))</pre>
<p>This allows us to go from 5 hours to around 30 seconds without the morphological operation, and around 3 minutes with it for a full volume. This could be further accelerated with some multithreading, but I don’t really need the speed at the moment, so that exercise will be left for another time.</p>
<p>Now that we have our graph, we can also filter any supervoxel that has a mean value outside of the expected range. Ideally, this range should be computed automatically, but for the moment I just use the 130-200 range that I previously defined.</p>
<h3 id="iterative-merging">Iterative merging</h3>
<p>For the last part of the method, I use a much simpler heuristic than the “graph cut” from (<a href="#ref-wu2016" role="doc-biblioref">Wu et al. 2016</a>). First, I build a list of all connections between neighbours, sorted based on the absolute difference in mean values. The first “connection” will therefore be between the two neighbouring supervoxels that are the most similar in terms of mean intensity. These two supervoxels are then “merged”: the smallest of the two is “consumed” by the largest (and its neighbours list is also merged with the other’s). Then we iterate through the sorted list of connections.</p>
<p>Two additional parameters control this merging process: a MAX_VOLUME, so that we don’t merge two supervoxels if it would result in an object that’s too large, and a MAX_DISS so that, when we reach a connection that has a dissimilarity above a given threshold (I used 10 in my experiments), we stop.</p>
<p>We then consider that the largest remaining supervoxel is our target object.</p>

<img src="./img/devlog-slic-liver-segmentation/liver-4h-segmented.png" alt="Largest supervoxel, reconstructed in 3D Slicer." />Largest supervoxel, reconstructed in 3D Slicer.

<h3 id="conclusion">Conclusion</h3>
<p>The result is still a bit noisy, but I’m very happy with it given the overall simplicity of the approach. The main difficulty will probably come from the large… artery, I think? Basically: the contrast agent used to highlight the tissue is also present in the vascular system, and it’s therefore difficult to avoid taking part of that system along. It should however be possible to use some post-processing to detect regions where we are in a “tubular” shape, and filter them out.</p>
<p>For such “specialized” applications, creating the kind of datasets needed for deep learning methods would be extremely challenging, so it’s really interesting I think to keep some “old school” methods alive. There is still a fair amount of work required to make the pipeline more robust - and to validate it - but I think it works well as a proof-of-concept.</p>
<h3 class="unnumbered" id="references">References</h3>


Achanta, Radhakrishna, Appu Shaji, Kevin Smith, Aurélien Lucchi, Pascal Fua, and Sabine Süsstrunk. 2012. “SLIC Superpixels Compared to State-of-the-art Superpixel Methods.” IEEE Transactions on Pattern Analysis and Machine Intelligence 34 (11): 2274–82.


Wu, Weiwei, Zhuhuang Zhou, Shuicai Wu, and Yanhua Zhang. 2016. “Automatic Liver Segmentation on Volumetric CT Images Using Supervoxel-Based Graph Cuts.” Computational and Mathematical Methods in Medicine 2016: 1–14. <a href="https://doi.org/10.1155/2016/9093721">https://doi.org/10.1155/2016/9093721</a>.


</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-12-06</pubDate>
    </item><item>
    <title>[Preprint] Panoptic Quality : not always a good metric</title>
    <link>https://research.adfoucart.be/preprint-panoptic-quality</link>
    <description><p>This is the last preprint to come out of the work done during my thesis: “Why Panoptic Quality should be avoided as a metric for assessing cell nuclei segmentation and classification in digital pathology,” co-authored by my PhD advisors Christine Decaestecker and Olivier Debeir. The preprint is available on <a href="https://www.researchsquare.com/article/rs-2272573/v1">ResearchSquare</a>, or on <a href="./preprint/FOUCART22-PQ.pdf">this website</a>. The code to replicate the results and figures is available on <a href="https://github.com/adfoucart/panoptic-quality-suppl">GitHub</a>.</p>
<p>This completes a set of studies that we’ve done, largely but not only based on the results of the MoNuSAC nuclei instance segmentation and classification challenge. In the first one (<a href="#ref-foucart22a" role="doc-biblioref">Foucart, Debeir, and Decaestecker 2022a</a>) (<a href="./preprint/FOUCART22-Comment.pdf">PDF</a>), we discovered some errors in the challenge’s evaluation code, which I <a href="./monusac-error">previously talked about in this blog</a>. In the second one (<a href="#ref-foucart22b" role="doc-biblioref">Foucart, Debeir, and Decaestecker 2022b</a>) (<a href="./preprint/FOUCART22-MoNuSAC.pdf">PDF</a>), we looked at the problem of using “entangled” metrics (i.e. metrics that combine multiple independent subtasks into a single score) - such as Panoptic Quality - for ranking challenges. In this one, finally, we round up our analysis and demonstrate why this metric is inadequate - and should be abandoned - for the task of nuclei instance segmentation and classification, where it is becoming “standard.”</p>
<p>In addition to the previously mentioned problem of combining multiple metrics (in this case, the segmentation IoU with the detection F1-Score), we show that the problem that the metric is designed to evaluate, “Panoptic Segmentation” (<a href="#ref-kirillov19" role="doc-biblioref">Kirillov et al. 2019</a>), has some fundamental differences with the problem of “instance segmentation and classification” that we are trying to evaluate here. We also demonstrate that the IoU, used for the segmentation part, is inadequate for small objects with fuzzy, uncertain boundaries, such as nuclei.</p>
<p>Those three problems (entanglement, task mismatch and inadequate segmentation score) together should disqualify Panoptic Quality as a metric for ranking algorithms on the task of nuclei instance segmentation and classification.</p>
<h3 class="unnumbered" id="references">References</h3>


Foucart, Adrien, Olivier Debeir, and Christine Decaestecker. 2022a. “Comments on ‘MoNuSAC2020: A Multi-Organ Nuclei Segmentation and Classification Challenge’.” IEEE Transactions on Medical Imaging 41 (4): 997–99. <a href="https://doi.org/10.1109/TMI.2022.3156023">https://doi.org/10.1109/TMI.2022.3156023</a>.


———. 2022b. “Evaluating Participating Methods in Image Analysis Challenges: Lessons from MoNuSAC 2020.” <a href="https://doi.org/10.13140/RG.2.2.11627.00801">https://doi.org/10.13140/RG.2.2.11627.00801</a>.


Kirillov, Alexander, Kaiming He, Ross Girshick, Carsten Rother, and Piotr Dollar. 2019. “Panoptic Segmentation.” Proceedings of the IEEE Computer Society Conference on Computer Vision and Pattern Recognition 2019-June: 9396–9405. <a href="https://doi.org/10.1109/CVPR.2019.00963">https://doi.org/10.1109/CVPR.2019.00963</a>.


</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-11-28</pubDate>
    </item><item>
    <title>[Reading] Top ACROBATs - pathology registration</title>
    <link>https://research.adfoucart.be/reading-top-acrobats</link>
    <description><p>In the <a href="./soa-review-registration">reviews of medical image registration</a> that I previously summarized, the most common methods were clearly intensity-based. This means that they use the pixel intensity to measure the similarity between the moving image and the target image. The big advantage of such methods is that they are dense: every pixel contributes to the overall similarity and helps find the best transform.</p>
<p>This, however, is not the only available option, and it’s not always the best option. A key issue with intensity-based method is that it will typically not work well if there are real alterations between the two images, alterations meaning here that the images are fundamentally different in some aspect, and not just “warping” of each other. This is typically the case in digital pathology registration, where we may be working with adjacent slices of tissue (so that the individual cells are not the same from one slice to the next, even though the overall structure will be very similar), and/or with different stains (e.g. the “generic” H&amp;E stain for one, and more specific immunohistochemistry stains for the other).</p>
<p>This is probably why, in the recent <a href="https://acrobat.grand-challenge.org/">Acrobat challenge</a>, the top methods were rather feature-based. Let’s take a look at how they work.</p>
<h3 id="the-winner-possibly---marzhal-et-al.">The winner (possibly) - Marzhal et al.</h3>
<p>The winner of the challenge was Christian Marzahl, from a company called Gestalt Diagnostics. I didn’t find a description of the method his team used in the challenge… but last year he published a “Robust Quad-Tree based Registration on Whole Slide Images” (<a href="#ref-marzahl2021" role="doc-biblioref">Marzahl et al. 2021</a>), so I’m going to assume his Acrobat entry was based on the same principles.</p>
<p>So what did they do?</p>
<p>They used a Quad-Tree approach to “recursively divide the WSI into image segments with successively higher resolution levels,” then to do “piece-wise affine approximation of any non-linear deformation,” using matching SIFT keypoints to determine the transformation matrix.</p>
<p>So, in slightly less technical terms:</p>
<ol type="1">
<li>At low resolution, find keypoints in the full images.</li>
<li>Determine best “affine” transform (which include rotation, translation, scaling, shear…)</li>
<li>Recursively:
<ol type="1">
<li>Cut the image in quadrants.</li>
<li>Take quadrants at higher resolution.</li>
<li>Find keypoints &amp; best affine transform per-quadrant.</li>
<li>Repeat until either maximum “depth” is reached, or some “stopping criterion” is met.</li>
</ol></li>
</ol>
<p>Feature-based methods are therefore sparse: they only use a selection of keypoints to find the transform. The big advantage of using only keypoints is that regions where the tissue is damaged, or otherwise “too” different between the two images, will just not be taken into account in the computation of the transform. It’s much easier to discard outliers in this way. It’s also a lot less dependent on a “pre-registration” step. Even if the two images are badly misaligned or rotated, it’s not too difficult to find the “best transform” between sets of matching points.</p>
<p>The big disadvantage, of course, is that if the matching step is not good, the results are going to be useless.</p>
<p>The code from that publication is also available on GitHub: https://github.com/ChristianMarzahl/WsiRegistration, so it should be relatively easy to test on our images.</p>
<h3 id="the-runner-up-valis">The runner-up: VALIS</h3>
<p>The runner-up of the challenge was the team behind <a href="https://pypi.org/project/valis-wsi/">VALIS</a> (“Virtual Alignment of pathoLogy Image Series”), a digital pathology registration module. So let’s also take a look at their methods (<a href="#ref-gatenbee2021" role="doc-biblioref">Gatenbee et al. 2021</a>).</p>
<p>While Marzahl et al. were concerned with pairs of slices, VALIS aims at registering a full stack of N adjacent slices. Their pipeline contains three main parts: a pre-processing module, a feature-based rigid registration module, and an intensity-based non-rigid registration refinement module.</p>
Pre-processing
<p>Their pre-processing steps are:</p>
<ul>
<li>Resize all images to the same largest dimension.</li>
<li>Work on low resolution image</li>
<li>Re-coloring to make the image similar. First, the RGB images are transformed into the <a href="https://onlinelibrary.wiley.com/doi/10.1002/col.22131">CAM16-UCS</a> colorspace, then assigned a constant “hue” and “colorfulness,” then converted back to RGB. The resulting images are then finally converted to greyscale, inverted, and normalized so that “they have similar distributions of pixel values.”</li>
</ul>
<p>This normalization step is further explained:</p>
<blockquote>
<p>The normalization method is inspired by (Khan, Rajpoot, Treanor, &amp; Magee, 2014), where first the 5th percentile, average, and 95th percentile of all pixel values is determined. These target values are then used as knots in cubic interpolation, and then the pixel values of each image are fit to the target values.</p>
</blockquote>
Rigid registration
<p>Their rigid registration steps are:</p>
<ul>
<li>Keypoints detection (with <a href="https://margaritachli.com/papers/ICCV2011paper.pdf">BRISK</a>) &amp; feature extraction (with <a href="https://www.robots.ox.ac.uk/~vgg/publications/2014/Simonyan14/simonyan13a.pdf">VGG descriptors</a>).</li>
<li>Compute transform between adjacent slices. For the transform between slice i and slice i-1, only the features from keypoints that are also matched in slice i+1 are used.</li>
</ul>
Non-rigid transformation
<p>This step is done after the rigid registration. They use existing methods, which they don’t describe further:</p>
<blockquote>
<p>VALIS can conduct this non-rigid registration using one of three methods: Deep Flow, SimpleElastix, or Groupwise SimpleElastix.</p>
</blockquote>
<p>Their results tend to show that the improvement from the non-rigid transformation is relatively small.</p>
<h3 id="conclusions">Conclusions?</h3>
<p>It’s interesting to see that the two best methods on this particular challenge are relying on relatively “old” techniques with keypoints, descriptors, and “good old” image processing. It seems again that deep learning methods don’t show the same dominance for this task as they usually do in image analysis.</p>
<p>The good performance of feature-based methods is something to keep in mind, particularly when digital pathology is involved.</p>
<h3 class="unnumbered" id="references">References</h3>


Gatenbee, Chandler D., Ann-Marie Baker, Sandhya Prabhakaran, Robbert J. C. Slebos, Gunjan Mandal, Eoghan Mulholland, Simon Leedham, et al. 2021. “VALIS: Virtual Alignment of pathoLogy Image Series.” Preprint. Cancer Biology. <a href="https://doi.org/10.1101/2021.11.09.467917">https://doi.org/10.1101/2021.11.09.467917</a>.


Marzahl, Christian, Frauke Wilm, Franz Dressler, Lars Tharun, Sven Perner, Christof Bertram, Christine Kröger, et al. 2021. “Robust Quad-Tree Based Registration on Whole Slide Images.” In Proceedings of Machine Learning Research, 156:181–90. <a href="https://proceedings.mlr.press/v156/marzahl21a.html">https://proceedings.mlr.press/v156/marzahl21a.html</a>.


</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-11-24</pubDate>
    </item><item>
    <title>[Opinion] The Galactica debacle</title>
    <link>https://research.adfoucart.be/opinion-galactica</link>
    <description><p>So I’ve made clear <a href="./ai-wrong-path">in the past</a> that I’m not a huge fan of large language models as a way forward for AI. I think we just got a great example of the fundamental problems of this approach with the “Galactica” fiasco. A decent summary of the events by Will Douglas Heaven was posted on <a href="https://www.technologyreview.com/2022/11/18/1063487/meta-large-language-model-ai-only-survived-three-days-gpt-3-science/">MIT Technology Review</a>. I just want to give here my quick summary and personal experience, and what it all means going forward.</p>
<h3 id="what-the-hell-is-galactica-anyway">What the hell is Galactica, anyway?</h3>
<p>On November 15th, Galactica was introduced by “Papers with Code” (a Meta AI project) on their social media accounts (<a href="https://twitter.com/paperswithcode/status/1592546933679476736">Twitter thread</a>, <a href="https://www.linkedin.com/feed/update/urn:li:activity:6998315655317889024?updateEntityUrn=urn%3Ali%3Afs_feedUpdate%3A%28V2%2Curn%3Ali%3Aactivity%3A6998315655317889024%29">LinkedIn post</a>) and a now unavailable live demo. In case Twitter no longer exists by the time you read this post, their main claims were:</p>
<blockquote>
<p>Can summarize academic literature, solve math problems, generate Wiki articles, write scientific code, annotate molecules and proteins, and more.</p>
</blockquote>
<p>They talked a bit about their corpus of data:</p>
<blockquote>
<p>We train on a large scientific corpus of papers, reference material, knowledge bases and many other sources. Includes scientific text and also scientific modalities such as proteins, compounds and more.</p>
</blockquote>
<h3 id="how-well-did-it-work">How well did it work?</h3>
<p>As you may expect from how quickly it was pulled: not very well. I tried a couple of queries when I saw the post, just to get an idea, and it was not particularly impressive in terms of “summarizing academic literature” or “write scientific code”, the two things that I briefly tried.</p>
<p>I tried to get it to make a review of segmentation methods and a few other basic image analysis concepts, and got badly written, repetitive and completely unsourced (and therefore unusable for any scientific purpose) stuff that was not necessarily “false”, but at best very surface level. Surface level would be fine if it then linked to wherever we could learn more, but in its current state at least it just had no added value over just going to the wikipedia page of whatever concept you want to write about. Or Google.</p>
<p>I also tried to get it to code a Otsu thresholding algorithm, and the code was messy and filled with mistakes (as in: not keeping its variable names straight, importing a bunch of useless things, etc.) It was clearly lifting things from a few different codes from its training set that had Otsu in it with no idea on what it was supposed to be doing.</p>
<p>As many others did, I also tested it on some more “politically charged” stuff and it miserably failed to detect that it was making a spirited defense of the Third Reich that maybe should have raised a red flag somewhere in the system.</p>
<p>It’s just baffling that Meta thought this was ready for a public demo.</p>
<h3 id="so-what-happened">So, what happened?</h3>
<p>Reading the <a href="https://arxiv.org/abs/2211.09085">Arxiv</a> preprint, the most charitable reading of the situation I can give is that there has been some, let’s say, misalignment between the research team that developed and validated the model, and the people that made and advertised the demo website. I don’t know if it’s the same people, but if it’s not, it may explain a few things.</p>
<p>Because the thing is, reading the original paper, it really doesn’t look like it’s supposed to “summarize academic literature” or “generate Wiki articles” or “write scientific code”. Certainly, the <a href="https://twitter.com/ylecun/status/1592619400024428544">claims</a> by Yann LeCun that you can “[t]ype a text and http://galactica.ai will generate a paper with relevant references, formulas, and everything” is just plain wrong.</p>
<p>The benchmark that they used to validate the model are on very specific, specialized tasks, like “prompt with an equation name and generate LaTeX” (if you need that, use <a href="https://www.wolframalpha.com">WolframAlpha</a>!), describe proteins from an amino-acid sequence, or answer very specific questions (e.g. prompt “Abell 370 is a galaxy cluster located in the constellation of”, answer “Cetus”).</p>
<p>There are a handful of examples in the appendix of larger texts with “Wikipedia” articles and “literature survey”, but they seem very handpicked and there doesn’t appear to have been a systematic evaluation for those.</p>
<p>And as soon as real users got to play with the prompts, the whole thing unraveled.</p>
<h3 id="what-can-we-learn-from-this">What can we learn from this?</h3>
<p>Large language models, like image generators, are cool. But they are also dumb. Even when they are very large and get very impressive scores and curated benchmarks, they don’t have any real knowledge or understanding. They generate stuff that’s likely to be “acceptable” by a human as “something another human could say”. On very specific prompts, they will be able to get the right answer because the mapping between the tokenized input and the expected output will be very clear (in other words, it’s unlikely that there will be many other words other than “Cetus” with a strong association with the words “Abell 370 is a galaxy cluster located in the constellation of” in the dataset). But anything more generic, more like what the announcement implied it could do… just doesn’t work. Or if it does, it’s very likely to be similar to DALL-E and other image generators: to really be able to use it, you have to learn its language. How to tweak the prompts exactly right so that you activate the right path through the model.</p>
<p>But again: if you can do that, you should be able to learn how to use Wikipedia and Google Scholar, where your risk of plagiarizing someone else’s work by accident will be much reduced…</p>
<p>In summary: beware of claims from large language models that have not been thoroughly tested “in the wild”, because that’s the only place you can actually test them (at least if you claim that they can be useful to the general public).</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-11-19</pubDate>
    </item><item>
    <title>[Devlog] Coordinates and scale</title>
    <link>https://research.adfoucart.be/devlog-coordinates-scale</link>
    <description><h3 id="part-1---why-its-complicated">Part 1 - why it’s complicated</h3>
<p>When working with digital pathology images, particularly in a context where we have to use multiple libraries, compare to different modalities, and work at different scale, it’s very easy to get lost in the coordinates systems. It’s (for me at least) an extremely common source of error in my code, where I mix my axes or screw up in some way the indexing of the region I’m trying to adress.</p>
<p>Let’s take a quick example to see what I mean. To open whole-slide images (WSI), I use the <a href="https://openslide.org">Openslide</a> library:</p>
<pre class="sourceCode python"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>import openslide
<a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>slide = openslide.OpenSlide(&quot;path/to/slide.ndpi&quot;)</pre>
<p>These WSI are multi-scale, meaning that they have been scanned at different levels of magnifications in the microscope. From the OpenSlide object, we can get information about the available levels, magnification, and corresponding resolution.</p>
<pre class="sourceCode python"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>max_mag = float(slide.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER])
<a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>mpp_x = float(slide.properties[openslide.PROPERTY_NAME_MPP_X])
<a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>mpp_y = float(slide.properties[openslide.PROPERTY_NAME_MPP_Y])
<a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>
<a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>print(f&quot;Maximum magnification: {max_mag}x&quot;)
<a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>print(f&quot;Maximum resolution: {mpp_x:.2f}x{mpp_y:.2f}µm/px&quot;)
<a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>print(f&quot;Number of levels: {slide.level_count}&quot;)
<a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>for level in range(slide.level_count):
<a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>    print(f&quot;[{level}] {slide.level_dimensions[level]}px\t&quot; +
<a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>          f&quot;@{max_mag/slide.level_downsamples[level]:.2f}x\t&quot; +
<a href="#cb2-11" aria-hidden="true" tabindex="-1"></a>          f&quot;{mpp_x*slide.level_downsamples[level]:.2f}x{mpp_y*slide.level_downsamples[level]:.2f}µm/px (&quot; +
<a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>          f&quot;downsample={slide.level_downsamples[level]})&quot;)</pre>
<pre class='outputCell'>Maximum magnification: 20.0x
Maximum resolution: 0.46x0.46µm/px
Number of levels: 8
[0] (76800, 38016)px    @20.00x 0.46x0.46µm/px (downsample=1.0)
[1] (38400, 19008)px    @10.00x 0.92x0.92µm/px (downsample=2.0)
[2] (19200, 9504)px @5.00x  1.84x1.84µm/px (downsample=4.0)
[3] (9600, 4752)px  @2.50x  3.67x3.67µm/px (downsample=8.0)
[4] (4800, 2376)px  @1.25x  7.35x7.35µm/px (downsample=16.0)
[5] (2400, 1188)px  @0.62x  14.69x14.69µm/px (downsample=32.0)
[6] (1200, 594)px   @0.31x  29.38x29.38µm/px (downsample=64.0)
[7] (600, 297)px    @0.16x  58.76x58.77µm/px (downsample=128.0)</pre>
<p>At this point, we have not yet accessed the image data. If we want to do that, we need to call the read_region method, which takes as input three parameters: the location (of the top-left corner), the level and the dimensions. Here, we get a first common source of error, which is that the location needs to be expressed in “pixels at the highest available resolutions” (i.e. level 0), while the dimension is expressed at the target level. In both cases, the convention used for the axis order is (x, y), with x being the “width” direction and y the “height”.</p>
<p>So, let’s first look at the whole image at the lowest level of magnification:</p>
<pre class="sourceCode python"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>from matplotlib import pyplot as plt
<a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>%matplotlib inline
<a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>
<a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>im_low = slide.read_region((0, 0), 7, (600, 297))
<a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>
<a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>plt.imshow(im_low)
<a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-coordinates-scale/output_5_0.png" alt="Full image at lowest available magnification (~59x59µm/px)" />Full image at lowest available magnification (~59x59µm/px)

<p>Now if we want to look more specifically at the tissue region on the left at a higher magnification (say, 1.25x, corresponding to “level 4” in this image), we have some computations to do. We can easily determine good coordinates and dimensions at the level that we’re currently looking at. Something like (10, 130) for the top-left corner, (120, 150) for the dimensions should work. But we need to convert those values to the right coordinates systems. For the location, it’s the highest available resolution, which is 128x higher than the one we were looking at.</p>
<p>So we need:</p>
<pre class="sourceCode python"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>location = (10*128, 130*128)</pre>
<p>And for the dimensions, we need to look at level 4, which is 8x more magnification than the current view, so we need:</p>
<pre class="sourceCode python"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>dimensions = (120*8, 150*8)</pre>
<p>Let’s check if our math was correct:</p>
<pre class="sourceCode python"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>tissue_region = slide.read_region(location, 4, dimensions)
<a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>plt.imshow(tissue_region)
<a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-coordinates-scale/output_11_0.png" alt="Tissue region @1.25x magnification (~7x7µm/px)" />Tissue region @1.25x magnification (~7x7µm/px)

<p>Perfect… but as you can see, it requires some thinking to make sure we get the correct region. And it gets worse. Because read_region returns an <a href="https://pillow.readthedocs.io/en/stable/reference/Image.html">Image</a> object from the PIL library, but very often when we do some actual processing (or to display it like we just did with matplotlib) we want to have access directly to the pixel data, which we will typically store in a numpy array. The conversion is easily done, but we can see something interesting when we print the shape of this array:</p>
<pre class="sourceCode python"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>import numpy as np
<a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>tissue_region_array = np.array(tissue_region)
<a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>print(tissue_region_array.shape)</pre>
<pre class='outputCell'>(1200, 960, 4)</pre>
<p>The dimensions are now expressed with a different convention: the first axis is the “rows” (or height), the second the “columns” (width), and then we have 4 channels, as the image has been opened as an RGBA image, with an Alpha channel. So if we want to adress a region in this array, we have to use a (y, x) convention instead of (x, y).</p>
<p>That’s not all. For registration tasks, a very useful library is <a href="https://simpleitk.org">SimpleITK</a>. Which has, of course, its own image format, which can cause some trouble if we’re not careful:</p>
<pre class="sourceCode python"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>import SimpleITK as sitk
<a href="#cb10-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>tissue_region_sitk = sitk.GetImageFromArray(tissue_region_array)
<a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>print(tissue_region_sitk.GetSize())
<a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>print(tissue_region_sitk.GetWidth(), tissue_region_sitk.GetHeight(), tissue_region_sitk.GetDepth())</pre>
<pre class='outputCell'>(4, 960, 1200)
4 960 1200</pre>
<p>As we can see here, the method to read an image from a numpy array interpreted the numpy dimensions as (depth, height, width), which is not ideal if we want to process the image in SimpleITK… If we want SimpleITK to give the expected results, we need to specify that the pixel values are vectors (here, RGBA vectors):</p>
<pre class="sourceCode python"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a>tissue_region_sitk = sitk.GetImageFromArray(tissue_region_array, isVector=True)
<a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>print(tissue_region_sitk.GetSize())
<a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>print(tissue_region_sitk.GetWidth(), tissue_region_sitk.GetHeight(), tissue_region_sitk.GetDepth())</pre>
<pre class='outputCell'>(960, 1200)
960 1200 0</pre>
<p>And we are back to the (x, y) convention, like PIL. So if we want to access the same pixel in the three representations of the tissue region, we have:</p>
<pre class="sourceCode python"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a>px_pil = tissue_region.getpixel((400, 300))
<a href="#cb14-2" aria-hidden="true" tabindex="-1"></a>px_np = tissue_region_array[300, 400]
<a href="#cb14-3" aria-hidden="true" tabindex="-1"></a>px_sitk = tissue_region_sitk.GetPixel(400, 300)
<a href="#cb14-4" aria-hidden="true" tabindex="-1"></a>print(px_pil)
<a href="#cb14-5" aria-hidden="true" tabindex="-1"></a>print(px_np)
<a href="#cb14-6" aria-hidden="true" tabindex="-1"></a>print(px_sitk)</pre>
<pre class='outputCell'>(181, 84, 151, 255)
[181  84 151 255]
(181, 84, 151, 255)</pre>
<p>Another interesting observation is that, in sitk, we can bring back some information about the location and the “real-world” resolution of the image, informations that are completely lost in the PIL and Numpy representations. With the “Origin” and “Spacing”, we can put the image into a world representation. Let’s do it here using µm for our dimensions.</p>
<pre class="sourceCode python"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a>tissue_region_sitk.SetOrigin((10*128*mpp_x, 130*128*mpp_y)) # offset in µm from the top-left corner of the image
<a href="#cb16-2" aria-hidden="true" tabindex="-1"></a>tissue_region_sitk.SetSpacing((16*mpp_x, 16*mpp_y)) # resolution in µm at the level 4 (which is 16x downsampled from the maximum)</pre>
<p>And we can see that all these informations are contained into the sitk.Image object.</p>
<pre class="sourceCode python"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a>print(tissue_region_sitk)</pre>
<pre class='outputCell'>VectorImage (0000028FFFD0E2E0)
  RTTI typeinfo:   class itk::VectorImage&lt;unsigned char,2&gt;
  Reference Count: 1
  Modified Time: 1676
  Debug: Off
  Object Name: 
  Observers: 
    none
  Source: (none)
  Source output name: (none)
  Release Data: Off
  Data Released: False
  Global Release Data: Off
  PipelineMTime: 0
  UpdateMTime: 0
  RealTimeStamp: 0 seconds 
  LargestPossibleRegion: 
    Dimension: 2
    Index: [0, 0]
    Size: [960, 1200]
  BufferedRegion: 
    Dimension: 2
    Index: [0, 0]
    Size: [960, 1200]
  RequestedRegion: 
    Dimension: 2
    Index: [0, 0]
    Size: [960, 1200]
  Spacing: [7.34518, 7.34585]
  Origin: [587.614, 7639.69]
  Direction: 
1 0
0 1

  IndexToPointMatrix: 
7.34518 0
0 7.34585

  PointToIndexMatrix: 
0.136144 0
0 0.136131

  Inverse Direction: 
1 0
0 1

  VectorLength: 4
  PixelContainer: 
    ImportImageContainer (0000028F825273F0)
      RTTI typeinfo:   class itk::ImportImageContainer&lt;unsigned __int64,unsigned char&gt;
      Reference Count: 1
      Modified Time: 1673
      Debug: Off
      Object Name: 
      Observers: 
        none
      Pointer: 0000028F883B1040
      Container manages memory: true
      Size: 4608000
      Capacity: 4608000</pre>
<h3 id="part-2---how-can-we-manage-it-more-easily">Part 2 - how can we manage it more easily?</h3>
<p>Clearly, this needs to be managed in some way. Here’s what I’ve been doing at the moment. First, I want to make sure that I can use the coordinates system more explicitly. So I want to define a “vector” class for coordinates and dimensions representation, using dataclasses (if you want more on dataclasses and why they’re useful, I recommend <a href="https://www.youtube.com/watch?v=CvQ7e6yUtnw">ArjanCodes’ video on YouTube</a> on the topic):</p>
<pre class="sourceCode python"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a>from dataclasses import dataclass
<a href="#cb19-2" aria-hidden="true" tabindex="-1"></a>from typing import Union
<a href="#cb19-3" aria-hidden="true" tabindex="-1"></a>
<a href="#cb19-4" aria-hidden="true" tabindex="-1"></a>@dataclass
<a href="#cb19-5" aria-hidden="true" tabindex="-1"></a>class Vector2D:
<a href="#cb19-6" aria-hidden="true" tabindex="-1"></a>    x: Union[int, float]
<a href="#cb19-7" aria-hidden="true" tabindex="-1"></a>    y: Union[int, float]
<a href="#cb19-8" aria-hidden="true" tabindex="-1"></a>    
<a href="#cb19-9" aria-hidden="true" tabindex="-1"></a>    @property
<a href="#cb19-10" aria-hidden="true" tabindex="-1"></a>    def xy(self):
<a href="#cb19-11" aria-hidden="true" tabindex="-1"></a>        return self.x, self.y
<a href="#cb19-12" aria-hidden="true" tabindex="-1"></a>    
<a href="#cb19-13" aria-hidden="true" tabindex="-1"></a>    @property
<a href="#cb19-14" aria-hidden="true" tabindex="-1"></a>    def yx(self):
<a href="#cb19-15" aria-hidden="true" tabindex="-1"></a>        return self.y, self.x
<a href="#cb19-16" aria-hidden="true" tabindex="-1"></a>
<a href="#cb19-17" aria-hidden="true" tabindex="-1"></a>coordinates = Vector2D(x=20, y=130)
<a href="#cb19-18" aria-hidden="true" tabindex="-1"></a>print(coordinates.xy, coordinates.yx)</pre>
<pre class='outputCell'>(20, 130) (130, 20)</pre>
<p>Next, I want to make the “navigation” in the WSI more transparent. For that, I’ll wrap the OpenSlide class into a WholeSlide class. There are several “helper” functions that I want to add there:</p>
<ul>
<li>Being able to find the “level” for a target magnification.</li>
<li>Being able to extract regions based on relative coordinates (so that I can easily find the same region at different scales).</li>
</ul>
<pre class="sourceCode python"><a href="#cb21-1" aria-hidden="true" tabindex="-1"></a>class WholeSlide:
<a href="#cb21-2" aria-hidden="true" tabindex="-1"></a>    def __init__(self, path: str):
<a href="#cb21-3" aria-hidden="true" tabindex="-1"></a>        self.path = path
<a href="#cb21-4" aria-hidden="true" tabindex="-1"></a>        self.slide = openslide.OpenSlide(path)
<a href="#cb21-5" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb21-6" aria-hidden="true" tabindex="-1"></a>        if openslide.PROPERTY_NAME_OBJECTIVE_POWER in self.slide.properties:
<a href="#cb21-7" aria-hidden="true" tabindex="-1"></a>            self.mag = float(self.slide.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER])
<a href="#cb21-8" aria-hidden="true" tabindex="-1"></a>        else:
<a href="#cb21-9" aria-hidden="true" tabindex="-1"></a>            self.mag = 1
<a href="#cb21-10" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb21-11" aria-hidden="true" tabindex="-1"></a>        if openslide.PROPERTY_NAME_MPP_X in self.slide.properties \
<a href="#cb21-12" aria-hidden="true" tabindex="-1"></a>                and openslide.PROPERTY_NAME_MPP_Y in self.slide.properties:
<a href="#cb21-13" aria-hidden="true" tabindex="-1"></a>            self.mpp = Vector2D(x=float(self.slide.properties[openslide.PROPERTY_NAME_MPP_X]),
<a href="#cb21-14" aria-hidden="true" tabindex="-1"></a>                                y=float(self.slide.properties[openslide.PROPERTY_NAME_MPP_Y]))
<a href="#cb21-15" aria-hidden="true" tabindex="-1"></a>        else:
<a href="#cb21-16" aria-hidden="true" tabindex="-1"></a>            self.mpp = Vector2D(x=1., y=1.)
<a href="#cb21-17" aria-hidden="true" tabindex="-1"></a>
<a href="#cb21-18" aria-hidden="true" tabindex="-1"></a>        self.dimensions = Vector2D(x=self.slide.level_dimensions[0][0], y=self.slide.level_dimensions[0][1]) 
<a href="#cb21-19" aria-hidden="true" tabindex="-1"></a>    
<a href="#cb21-20" aria-hidden="true" tabindex="-1"></a>    def get_best_level_for_magnification(self, magnification: float):
<a href="#cb21-21" aria-hidden="true" tabindex="-1"></a>        return self.slide.get_best_level_for_downsample(self.mag/magnification)
<a href="#cb21-22" aria-hidden="true" tabindex="-1"></a>    
<a href="#cb21-23" aria-hidden="true" tabindex="-1"></a>    def get_absolute_position(self, location: Vector2D):
<a href="#cb21-24" aria-hidden="true" tabindex="-1"></a>        &quot;&quot;&quot;Returns location in µm from the top-left corner&quot;&quot;&quot;
<a href="#cb21-25" aria-hidden="true" tabindex="-1"></a>        return Vector2D(x=location.x*self.dimensions.x*self.mpp.x,
<a href="#cb21-26" aria-hidden="true" tabindex="-1"></a>                        y=location.y*self.dimensions.y*self.mpp.y)
<a href="#cb21-27" aria-hidden="true" tabindex="-1"></a>    
<a href="#cb21-28" aria-hidden="true" tabindex="-1"></a>    def read_region(self, location: Vector2D, magnification: float, dimensions: Vector2D):
<a href="#cb21-29" aria-hidden="true" tabindex="-1"></a>        level = self.get_best_level_for_magnification(magnification)
<a href="#cb21-30" aria-hidden="true" tabindex="-1"></a>        dimensions_at_level = Vector2D(x=int(dimensions.x*self.slide.level_dimensions[level][0]),
<a href="#cb21-31" aria-hidden="true" tabindex="-1"></a>                                       y=int(dimensions.y*self.slide.level_dimensions[level][1]))
<a href="#cb21-32" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb21-33" aria-hidden="true" tabindex="-1"></a>        location_abs = Vector2D(x=int(location.x*self.dimensions.x), y=int(location.y*self.dimensions.y))
<a href="#cb21-34" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb21-35" aria-hidden="true" tabindex="-1"></a>        # check if we need to further rescale to get to the target magnification or if we can use the level as is
<a href="#cb21-36" aria-hidden="true" tabindex="-1"></a>        if self.slide.level_downsamples[level] == self.mag/magnification:
<a href="#cb21-37" aria-hidden="true" tabindex="-1"></a>            return self.slide.read_region(location_abs.xy, level, dimensions_at_level.xy)
<a href="#cb21-38" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb21-39" aria-hidden="true" tabindex="-1"></a>        print(f&quot;Resizing from {self.mag/self.slide.level_downsamples[level]}x&quot;)
<a href="#cb21-40" aria-hidden="true" tabindex="-1"></a>        region = self.slide.read_region(location_abs.xy, level, dimensions_at_level.xy)
<a href="#cb21-41" aria-hidden="true" tabindex="-1"></a>        factor = self.slide.level_downsamples[level]/(self.mag/magnification)
<a href="#cb21-42" aria-hidden="true" tabindex="-1"></a>        target_dimensions = Vector2D(x=int(dimensions_at_level.x*factor), y=int(dimensions_at_level.y*factor))
<a href="#cb21-43" aria-hidden="true" tabindex="-1"></a>        return region.resize(target_dimensions.xy)</pre>
<p>Let’s check that we can get the full image, and the same region at different scales using our relative coordinates:</p>
<pre class="sourceCode python"><a href="#cb22-1" aria-hidden="true" tabindex="-1"></a>wsi = WholeSlide(&quot;path/to/slide.ndpi&quot;)</pre>
<p>Full image:</p>
<pre class="sourceCode python"><a href="#cb23-1" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb23-2" aria-hidden="true" tabindex="-1"></a>plt.imshow(wsi.read_region(location=Vector2D(0, 0), magnification=20/128, dimensions=Vector2D(x=1, y=1)))
<a href="#cb23-3" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-coordinates-scale/output_31_0.png" alt="Full image at lowest available magnification (~59x59µm/px)" />Full image at lowest available magnification (~59x59µm/px)

<p>Region @1.25x, 1x, 2.5x</p>
<pre class="sourceCode python"><a href="#cb24-1" aria-hidden="true" tabindex="-1"></a>region_location = Vector2D(x=0.015, y=0.43)
<a href="#cb24-2" aria-hidden="true" tabindex="-1"></a>region_dimensions = Vector2D(x=0.2, y=0.5)
<a href="#cb24-3" aria-hidden="true" tabindex="-1"></a>
<a href="#cb24-4" aria-hidden="true" tabindex="-1"></a>print(f&quot;Loaction={wsi.get_absolute_position(region_location)}&quot;)
<a href="#cb24-5" aria-hidden="true" tabindex="-1"></a>
<a href="#cb24-6" aria-hidden="true" tabindex="-1"></a>plt.figure(figsize=(18,7))
<a href="#cb24-7" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 1)
<a href="#cb24-8" aria-hidden="true" tabindex="-1"></a>plt.imshow(wsi.read_region(location=region_location, magnification=1.25, dimensions=region_dimensions))
<a href="#cb24-9" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 2)
<a href="#cb24-10" aria-hidden="true" tabindex="-1"></a>plt.imshow(wsi.read_region(location=region_location, magnification=1., dimensions=region_dimensions))
<a href="#cb24-11" aria-hidden="true" tabindex="-1"></a>plt.subplot(1, 3, 3)
<a href="#cb24-12" aria-hidden="true" tabindex="-1"></a>plt.imshow(wsi.read_region(location=region_location, magnification=2.5, dimensions=region_dimensions))
<a href="#cb24-13" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>
<pre class='outputCell'>Loaction=Vector2D(x=528.8527750998485, y=7505.109958220467)
Resizing from 1.25x</pre>

<img src="./img/devlog-coordinates-scale/output_33_1.png" alt="Tissue region @1.25x, 1x and 2.5x magnification" />Tissue region @1.25x, 1x and 2.5x magnification

<p>That’s already a lot more convenient. Now we still have to deal with the multi-library situation. What I really want is to make sure that, when I extract a region, I can keep all the relevant information (offset, magnification) along the way, and I can get the image in any format I want. So for that I’m going to create a “SlideRegion” class, and move the read_region code here into a _load method. I’ll also add as_pil, as_np and as_sitk methods to get the images in the requested formats.</p>
<pre class="sourceCode python"><a href="#cb26-1" aria-hidden="true" tabindex="-1"></a>class SlideRegion:
<a href="#cb26-2" aria-hidden="true" tabindex="-1"></a>    def __init__(self, *,
<a href="#cb26-3" aria-hidden="true" tabindex="-1"></a>                 wsi: WholeSlide,
<a href="#cb26-4" aria-hidden="true" tabindex="-1"></a>                 location: Vector2D,
<a href="#cb26-5" aria-hidden="true" tabindex="-1"></a>                 dimensions: Vector2D,
<a href="#cb26-6" aria-hidden="true" tabindex="-1"></a>                 magnification: float):
<a href="#cb26-7" aria-hidden="true" tabindex="-1"></a>        self.wsi = wsi
<a href="#cb26-8" aria-hidden="true" tabindex="-1"></a>        self.location = location
<a href="#cb26-9" aria-hidden="true" tabindex="-1"></a>        self.dimensions = dimensions
<a href="#cb26-10" aria-hidden="true" tabindex="-1"></a>        self.magnification = magnification
<a href="#cb26-11" aria-hidden="true" tabindex="-1"></a>        self.pil = None
<a href="#cb26-12" aria-hidden="true" tabindex="-1"></a>
<a href="#cb26-13" aria-hidden="true" tabindex="-1"></a>    def _load(self):
<a href="#cb26-14" aria-hidden="true" tabindex="-1"></a>        level = self.wsi.get_best_level_for_magnification(self.magnification)
<a href="#cb26-15" aria-hidden="true" tabindex="-1"></a>        dimensions_at_level = Vector2D(x=int(self.dimensions.x*self.wsi.slide.level_dimensions[level][0]),
<a href="#cb26-16" aria-hidden="true" tabindex="-1"></a>                                       y=int(self.dimensions.y*self.wsi.slide.level_dimensions[level][1]))
<a href="#cb26-17" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb26-18" aria-hidden="true" tabindex="-1"></a>        location_abs = Vector2D(x=int(self.location.x*self.wsi.dimensions.x), 
<a href="#cb26-19" aria-hidden="true" tabindex="-1"></a>                                y=int(self.location.y*self.wsi.dimensions.y))
<a href="#cb26-20" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb26-21" aria-hidden="true" tabindex="-1"></a>        # check if we need to further rescale to get to the target magnification or if we can use the level as is
<a href="#cb26-22" aria-hidden="true" tabindex="-1"></a>        if self.wsi.slide.level_downsamples[level] == self.wsi.mag/self.magnification:
<a href="#cb26-23" aria-hidden="true" tabindex="-1"></a>            self.pil = self.wsi.slide.read_region(location_abs.xy, level, dimensions_at_level.xy)
<a href="#cb26-24" aria-hidden="true" tabindex="-1"></a>            return
<a href="#cb26-25" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb26-26" aria-hidden="true" tabindex="-1"></a>        print(f&quot;Resizing from {self.wsi.mag/self.wsi.slide.level_downsamples[level]}x&quot;)
<a href="#cb26-27" aria-hidden="true" tabindex="-1"></a>        region = self.wsi.slide.read_region(location_abs.xy, level, dimensions_at_level.xy)
<a href="#cb26-28" aria-hidden="true" tabindex="-1"></a>        factor = self.wsi.slide.level_downsamples[level]/(self.wsi.mag/self.magnification)
<a href="#cb26-29" aria-hidden="true" tabindex="-1"></a>        target_dimensions = Vector2D(x=int(dimensions_at_level.x*factor), y=int(dimensions_at_level.y*factor))
<a href="#cb26-30" aria-hidden="true" tabindex="-1"></a>        self.pil = region.resize(target_dimensions.xy)
<a href="#cb26-31" aria-hidden="true" tabindex="-1"></a>
<a href="#cb26-32" aria-hidden="true" tabindex="-1"></a>    def as_pil(self):
<a href="#cb26-33" aria-hidden="true" tabindex="-1"></a>        if self.pil is None:
<a href="#cb26-34" aria-hidden="true" tabindex="-1"></a>            self._load()
<a href="#cb26-35" aria-hidden="true" tabindex="-1"></a>        return self.pil
<a href="#cb26-36" aria-hidden="true" tabindex="-1"></a>
<a href="#cb26-37" aria-hidden="true" tabindex="-1"></a>    def as_np(self):
<a href="#cb26-38" aria-hidden="true" tabindex="-1"></a>        if self.pil is None:
<a href="#cb26-39" aria-hidden="true" tabindex="-1"></a>            self._load()
<a href="#cb26-40" aria-hidden="true" tabindex="-1"></a>        return np.array(self.pil)
<a href="#cb26-41" aria-hidden="true" tabindex="-1"></a>
<a href="#cb26-42" aria-hidden="true" tabindex="-1"></a>    def as_sitk(self, floating = False):
<a href="#cb26-43" aria-hidden="true" tabindex="-1"></a>        &quot;&quot;&quot;Floating parameters indicates if the spacing / offset should be set (if floating is False).&quot;&quot;&quot;
<a href="#cb26-44" aria-hidden="true" tabindex="-1"></a>        if self.pil is None:
<a href="#cb26-45" aria-hidden="true" tabindex="-1"></a>            self._load()
<a href="#cb26-46" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb26-47" aria-hidden="true" tabindex="-1"></a>        image = sitk.GetImageFromArray(np.array(self.pil), isVector=True)
<a href="#cb26-48" aria-hidden="true" tabindex="-1"></a>        
<a href="#cb26-49" aria-hidden="true" tabindex="-1"></a>        if not floating:
<a href="#cb26-50" aria-hidden="true" tabindex="-1"></a>            downsample = self.wsi.mag/self.magnification       
<a href="#cb26-51" aria-hidden="true" tabindex="-1"></a>            spacing = Vector2D(x=float(self.wsi.mpp.x) * downsample,
<a href="#cb26-52" aria-hidden="true" tabindex="-1"></a>                               y=float(self.wsi.mpp.y) * downsample)
<a href="#cb26-53" aria-hidden="true" tabindex="-1"></a>            origin = self.wsi.get_absolute_position(self.location)
<a href="#cb26-54" aria-hidden="true" tabindex="-1"></a>            
<a href="#cb26-55" aria-hidden="true" tabindex="-1"></a>            image.SetSpacing((spacing.x, spacing.y))
<a href="#cb26-56" aria-hidden="true" tabindex="-1"></a>            
<a href="#cb26-57" aria-hidden="true" tabindex="-1"></a>            image.SetOrigin((origin.x, origin.y))
<a href="#cb26-58" aria-hidden="true" tabindex="-1"></a>
<a href="#cb26-59" aria-hidden="true" tabindex="-1"></a>        return image
<a href="#cb26-60" aria-hidden="true" tabindex="-1"></a>    
<a href="#cb26-61" aria-hidden="true" tabindex="-1"></a>    def absolute_position(self):
<a href="#cb26-62" aria-hidden="true" tabindex="-1"></a>        return self.wsi.get_absolute_position(self.location)</pre>
<p>And we can now modify the WholeSlide class so that read_region returns a SlideRegion:</p>
<pre class="sourceCode python"><a href="#cb27-1" aria-hidden="true" tabindex="-1"></a>class WholeSlide:
<a href="#cb27-2" aria-hidden="true" tabindex="-1"></a>    ...
<a href="#cb27-3" aria-hidden="true" tabindex="-1"></a>    
<a href="#cb27-4" aria-hidden="true" tabindex="-1"></a>    def read_region(self, location: Vector2D, magnification: float, dimensions: Vector2D):
<a href="#cb27-5" aria-hidden="true" tabindex="-1"></a>        return SlideRegion(wsi=self, location=location, magnification=magnification, dimensions=dimensions)</pre>
<p>So now we can get our region in the format that we want, while keeping the relevant information:</p>
<pre class="sourceCode python"><a href="#cb28-1" aria-hidden="true" tabindex="-1"></a>wsi = WholeSlide(&quot;path/to/slide.ndpi&quot;)
<a href="#cb28-2" aria-hidden="true" tabindex="-1"></a>
<a href="#cb28-3" aria-hidden="true" tabindex="-1"></a>region_location = Vector2D(x=0.015, y=0.43)
<a href="#cb28-4" aria-hidden="true" tabindex="-1"></a>region_dimensions = Vector2D(x=0.2, y=0.5)
<a href="#cb28-5" aria-hidden="true" tabindex="-1"></a>
<a href="#cb28-6" aria-hidden="true" tabindex="-1"></a>region = wsi.read_region(location=region_location, dimensions=region_dimensions, magnification=1.25) </pre>
<pre class="sourceCode python"><a href="#cb29-1" aria-hidden="true" tabindex="-1"></a>pos = region.absolute_position()
<a href="#cb29-2" aria-hidden="true" tabindex="-1"></a>plt.figure()
<a href="#cb29-3" aria-hidden="true" tabindex="-1"></a>plt.imshow(region.as_np())
<a href="#cb29-4" aria-hidden="true" tabindex="-1"></a>plt.title(f&quot;Location=(x={pos.x:.2f},y={pos.y:.2f})µm @{region.magnification}x&quot;)
<a href="#cb29-5" aria-hidden="true" tabindex="-1"></a>plt.show()</pre>

<img src="./img/devlog-coordinates-scale/output_40_0.png" alt="Tissue region @1.25x magnification, with location information" />Tissue region @1.25x magnification, with location information

<p>We can check that if we get sitk images at different resolutions, we have the correct information about their origin &amp; spacing:</p>
<pre class="sourceCode python"><a href="#cb30-1" aria-hidden="true" tabindex="-1"></a>region_sitk = region.as_sitk()
<a href="#cb30-2" aria-hidden="true" tabindex="-1"></a>print(region_sitk.GetOrigin(), region_sitk.GetSpacing())
<a href="#cb30-3" aria-hidden="true" tabindex="-1"></a>region_lowres = wsi.read_region(location=region_location, dimensions=region_dimensions, magnification=0.05).as_sitk()
<a href="#cb30-4" aria-hidden="true" tabindex="-1"></a>print(region_lowres.GetOrigin(), region_lowres.GetSpacing())</pre>
<pre class='outputCell'>(528.8527750998485, 7505.109958220467) (7.34517743194234, 7.345851889261283)
Resizing from 0.15625x
(528.8527750998485, 7505.109958220467) (183.62943579855852, 183.64629723153206)</pre>
<p>This should make it a lot less likely for me to screw up in the future !</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-11-16</pubDate>
    </item><item>
    <title>[Reading] Reviews of medical image registration</title>
    <link>https://research.adfoucart.be/soa-review-registration</link>
    <description>
<p>My <a href="./new-project">new project</a> involves a lot of image registration, which is an image analysis task that I haven’t really worked on much before. The goal, in the end, is to be able to “put together” information coming from multiple modalities and taken at different times: CT scans, MRI, and also histology images. This means putting everything into the same frame of reference – a registration task.</p>

<img src="./img/goubran15.png" alt="“In-vivo” MR to “ex-vivo” MR to histology images registration, from (Goubran et al. 2015)" />“In-vivo” MR to “ex-vivo” MR to histology images registration, from (<a href="#ref-goubran2015" role="doc-biblioref">Goubran et al. 2015</a>)

<p>So I obviously need to do some reading. As a starting point, we will use three big reviews of medical image registration. The first is a 2014 review (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>). I always like to start from reviews from the pre-”deep learning” era when looking at a task that I’m less familiar with, as they generally give a better overview of the general pipeline and the range of possible approaches. To that, I add a chapter from the 2020 “Handbook of Medical Image Computing and Computer Assisted Intervention,” focusing on registration using “machine learning and deep learning” (<a href="#ref-cao2020" role="doc-biblioref">Cao et al. 2020</a>). The last one, also from 2020, surveys deep learning methods more specifically (<a href="#ref-haskins2020" role="doc-biblioref">Haskins, Kruger, and Yan 2020</a>).</p>
<h3 id="what-is-image-registration">What is image registration</h3>
<p>Image registration “can be defined as the process of aligning two or more images” (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>). These may come from different modalities (e.g. CT and MRI), different times (e.g. to monitor tumor growth)… They may be 2D images (e.g. successive slices of a tissue block in digital pathology) or 3D images (e.g. MRI volume). (<a href="#ref-cao2020" role="doc-biblioref">Cao et al. 2020</a>) put it more mathematically as finding the transformation \(\phi^*\) such that:</p>
<p>\[
\phi^*={argmin}_\phi {S(I_R, \phi(I_M))}
\]</p>
<p>Where \(I_R\) is the reference image (often called “fixed”), \(I_M\) is the floating image (or “moving”), and \(S\) is a “similarity metric” that measure show well the transformed image \(\phi(I_M)\) matches the reference.</p>
<p>So the main elements that we have to play with are:</p>
<ul>
<li>A transformation model</li>
<li>The similarity metric (how do we define what “matching” means)</li>
<li>An optimization strategy: how do we minimize \(S\) so that we can find \(\phi^*\).</li>
</ul>
<h3 id="transformation-models">Transformation models</h3>
<p>The transformation model determines how we can modify the moving image. Broadly speaking, we can make a distinction between global models — e.g. rigid or affine transforms — which apply a single operation on the whole “image matrix,” and local or deformable models, which can be expressed as a “deformation field” where each voxel is associated to a vector pointing to its “new” position in the transformed image.</p>
<p>Rigid transforms are often used as a pre-registration step, to broadly align the two images before refining with a more complex and/or local model.</p>
<h3 id="classic-similarity-metrics">Classic similarity metrics</h3>
<p>This is where a lot of the complexity lies – how do you define what counts as a “good match?” It’s particularly difficult in multi-modal problems, where the nature of the information in the two images may be very different. (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>) and (<a href="#ref-cao2020" role="doc-biblioref">Cao et al. 2020</a>) both have mostly the same list of “commonly used” intensity-based metrics:</p>
<ul>
<li>Sum of Squared Differences / Mean Squared Differences, which assumes that “the corresponding structures in both images should have identical intensities” (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>).</li>
<li>Correlation Ratio, (Normalized) Cross-Correlation, which assumes that “there is a linear relation between the intensities of the corresponding structures” (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>).</li>
<li>(Normalized) Mutual Information, which assumes that “there is a functional between the variables involved, e.g. between the intensities of both images” (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>).</li>
</ul>
<p>The latter is generally recommended for multimodal registration, as the relationship between the voxel intensities between, for instance, a CT image and a MRI will not be simply linear.</p>
<h3 id="optimization-strategies">Optimization strategies</h3>
<p>Usually — particularly for complex transforms — we’ll have some sort of iterative process such as a gradient descent algorithm. Pre-registration with a rigid transform tends to make the process easier (or, at the very least, faster).</p>

<img src="./img/oliveira12.png" alt="Typical intensity-based registration algorithm pipeline, from (Oliveira and Tavares 2014)" />Typical intensity-based registration algorithm pipeline, from (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>)

<h3 id="what-about-machine-deep-learning">What about machine / deep learning?</h3>
<p>Focusing on the multimodal registration problem, (<a href="#ref-cao2020" role="doc-biblioref">Cao et al. 2020</a>) and (<a href="#ref-haskins2020" role="doc-biblioref">Haskins, Kruger, and Yan 2020</a>) give us some insights on how/where machine learning can intervene in the process.</p>
<ul>
<li>Learning a similarity metric to improve an otherwise classic pipline. The idea is found for instance in a deep learning method proposed in (<a href="#ref-cheng2018" role="doc-biblioref">Cheng, Zhang, and Zheng 2018</a>). A CNN classifier is trained to “learn the correspondence of two image patches,” then the probability at the output of the CNN is used as a similarity score during new registration. An obvious difficulty is that this requires to have well aligned pairs of images as a training set.</li>
<li>Learning a common feature representation Here the idea is that, since the “intensity values” don’t have the same meaning, we should try to find an “in-between” feature space where the projections from matching features (from both images) are well correlated.</li>
<li>Learning an appearance mapping model between modalities, e.g. to generate a “pseudo-CT” from an MRI. This then reduces the problem to an easier, monomodal registration problem.</li>
<li>Fully learning the transformation itself is much more complicated. It may be possible for relatively simple models (e.g. learning the transformation matrix of an affine transform), but for a deformable model the supervision is way too impractical to create.</li>
</ul>
<h3 id="evaluation-of-the-results">Evaluation of the results</h3>
<p>The evaluation of the results is a particularly difficult problem for registration, and it’s a problem that I’ll probably look into a lot more deeply, as I’ve done for segmentation and classification methods before (<a href="#ref-foucart-phd" role="doc-biblioref">Foucart 2022</a>).</p>
<p>As Oliveira notes, “the image similarity measure optimisation can be used as a crude accuracy measure,” but “most similarity measures frequently used have no geometric/physical significance” (<a href="#ref-oliveira2014" role="doc-biblioref">Oliveira and Tavares 2014</a>). So the most common approach is to “manually identify a set of corresponding points in both input images (…) and use them to assess the registration accuracy.” This, however, means relying on an expert-provided “ground truth,” with all the problems that come along (this is where I point again to my thesis, I guess!)</p>
<p>He also mentions using the Dice similarity coefficient (which is the “per-pixel” F1 score, in classification/detection terms) to quantify “the amount of overlapping regions.”</p>
<p>Not mentioned but, I think, potentially useful as well in the same vein would be contour-based measures such as the Hausdorff’s Distance, or similar → for instance, using a border detector and then measuring that the main edges in the registered and target image are close to each other.</p>

<img src="./img/reviews-ex.gif" alt="Two slices from a mouse brain tissue block, roughly aligned manually here through my great Paint.net skills. How do we measure a “good” match? (Images acquired at the CMMI)" />Two slices from a mouse brain tissue block, roughly aligned manually here through my great Paint.net skills. How do we measure a “good” match? (Images acquired at the <a href="http://cmmi.be/">CMMI</a>)

<h3 id="conclusions">Conclusions</h3>
<p>So that wraps this first look at the state-of-the-art. It’s clear that a key difficulty here is that we have a problem with lots of possible choices and parameters and which is difficult to objectively evaluate or, even, objectively pose. As an example, one of the things we need to do in the project is to register histology slices that are separated by a distance of ~100µm. So the goal is not really to match one image to another: there is no actual “match” between the two, they are different parts of the object. Instead, we are trying to “correctly” place the tissue sections in the slides at their correct position and orientation in a 3D volume — and then register that to a CT image.</p>
<p>Getting a good looking results is one thing, but objectively validating that the results are “correct,” or even defining what “correct” means in this case… will be interesting.</p>
<h3 class="unnumbered" id="references">References</h3>


Cao, Xiaohuan, Jingfan Fan, Pei Dong, Sahar Ahmad, Pew-Thian Yap, and Dinggang Shen. 2020. “Image Registration Using Machine and Deep Learning.” In Handbook of Medical Image Computing and Computer Assisted Intervention, 319–42. Elsevier. <a href="https://doi.org/10.1016/B978-0-12-816176-0.00019-3">https://doi.org/10.1016/B978-0-12-816176-0.00019-3</a>.


Cheng, Xi, Li Zhang, and Yefeng Zheng. 2018. “Deep Similarity Learning for Multimodal Medical Images.” Computer Methods in Biomechanics and Biomedical Engineering: Imaging &amp; Visualization 6 (3): 248–52. <a href="https://doi.org/10.1080/21681163.2015.1135299">https://doi.org/10.1080/21681163.2015.1135299</a>.


Foucart, Adrien. 2022. “Impact of Real-World Annotations on the Training and Evaluation of Deep Learning Algorithms in Digital Pathology.” <a href="https://research.adfoucart.be/thesis/FOUCART_Adrien_dissertation.pdf">https://research.adfoucart.be/thesis/FOUCART_Adrien_dissertation.pdf</a>.


Goubran, Maged, Sandrine de Ribaupierre, Robert R. Hammond, Catherine Currie, Jorge G. Burneo, Andrew G. Parrent, Terry M. Peters, and Ali R. Khan. 2015. “Registration of in-Vivo to Ex-Vivo MRI of Surgically Resected Specimens: A Pipeline for Histology to in-Vivo Registration.” Journal of Neuroscience Methods 241 (February): 53–65. <a href="https://doi.org/10.1016/j.jneumeth.2014.12.005">https://doi.org/10.1016/j.jneumeth.2014.12.005</a>.


Haskins, Grant, Uwe Kruger, and Pingkun Yan. 2020. “Deep Learning in Medical Image Registration: A Survey.” Machine Vision and Applications 31 (1-2): 8. <a href="https://doi.org/10.1007/s00138-020-01060-x">https://doi.org/10.1007/s00138-020-01060-x</a>.


Oliveira, Francisco P. M., and João Manuel R. S. Tavares. 2014. “Medical Image Registration: A Review.” Computer Methods in Biomechanics and Biomedical Engineering 17 (2): 73–93. <a href="https://doi.org/10.1080/10255842.2012.670855">https://doi.org/10.1080/10255842.2012.670855</a>.


</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-11-10</pubDate>
    </item><item>
    <title>A new project</title>
    <link>https://research.adfoucart.be/new-project</link>
    <description><p>My <a href="https://research.adfoucart.be/phd-thesis">PhD thesis</a> is, finally, done.</p>
<p>This means some changes: for my career and, by extension, for this research blog. This post is therefore going to be way more “personal blog” than usual.</p>
<h3 id="my-new-job">My new job</h3>
<p>Until the end of October, I was a teaching assistant and a PhD student. I am now moving on as a post-doctoral researcher. I am, however, staying at the Université Libre de Bruxelles, and in the <a href="https://lisa.polytech.ulb.be">LISA</a> laboratory.</p>
<p>I will be working on the ProTherWal project: “Proton Therapy Research in Wallonia”. This is a big research project involving different french-speaking universities in Belgium. My job as a postdoctoral researcher in particular will be related to image analysis aspects. The focus will be on combining information coming from different image modalities: CT, PET, MRI, and histopathology.</p>
<p>There are some obvious links with what I’ve been doing before, but still a fairly significant change, and a good opportunity to learn new things.</p>
<h3 id="whats-next-for-this-blog">What’s next for this blog?</h3>
<p>I originally intended for this research blog to be a way to communicate on my PhD research. I haven’t really updated it quite as much as I would have liked to during the thesis, but I’m still mostly happy with what I’ve written here.</p>
<p>I’d like to continue to use this channel. I’d like, however, to use it slightly differently.</p>
<p>First, I think it’d be interesting to give a bit more space to the process of research. As I’m starting a new project, it’s a good opportunity to talk about how I go about organizing things, reading the state-of-the-art, etc. As always, the process of writing things down for this blog is largely intended as a useful exercise for me, which may be helpful to others as a byproduct. I think I’d like to update the blog more often, with generally shorter posts.</p>
<p>Second, I’ll probably also move a bit more often into the realm of opinions, as I’ve done in my “<a href="https://research.adfoucart.be/ai-wrong-path">Is AI on the wrong path</a>” post. There are many conversations to be had on the use of AI in medical imaging, and I hope that this blog can contribute (with a limited impact for sure) to these conversations.</p>
<h3 id="you-want-to-know-when-i-publish-something">You want to know when I publish something?</h3>
<p>That’s very nice of you, thank you :)</p>
<p>I’ve now added an <a href="./feed.php">RSS feed</a>, as the recent Twitter apocalypse has clearly renewed my interest in not depending on a particular social media for promoting what I write. I’ll also generally publish links to blog posts on my <a href="https://www.linkedin.com/in/adrien-foucart-57055041/">LinkedIn</a>. As long as I remain on Twitter, which may not be very long, I am still there <a href="https://twitter.com/adfoucart">@adfoucart</a>. I’m now trying out Mastodon as well, so it’s another way to find me <a href="https://social.sciences.re/@AFoucart">@AFoucart@social.sciences.re</a>.</p>
<p>Finally, if you want to get updates by email, I’ve setup a substack newsletter: <a href="https://adfoucart.substack.com/?r=2922w&amp;utm_campaign=pub&amp;utm_medium=web">https://adfoucart.substack.com</a>. The goal is to send at most one email per month if I published something on the blog in the meantime.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-11-07</pubDate>
    </item><item>
    <title>The thesis</title>
    <link>https://research.adfoucart.be/phd-thesis</link>
    <description><p>Seven years ago, I started working on my thesis.</p>

<p>On October 25th, 2022, I'll be publicly defending it. It took a bit of time, but I'm happy with the result!</p>

<p>The text is available from here: <a href="https://research.adfoucart.be/thesis/FOUCART_Adrien_Dissertation.pdf">[<img src='pdf.png' width="20px" /> Dissertation]</a></p>

<h3>What's inside?</h3>

<p>Here are the main topics covered in the thesis</p>

<ul>
    <li>A history of deep learning for image analysis, and a presentation of the main components of deep learning pipelines.</li>
    <li>A history of computer vision in digital pathology, and the state-of-the-art of digital pathology image analysis before deep learning.</li>
    <li>The state-of-the-art of deep learning in digital pathology and its applications: mitosis detection, tumour classification/scoring, etc...</li>
    <li>A study of evaluation metrics and processes: which metrics are used in digital pathology challenges, what are their behaviours and limitations, how can we choose the right metric for the right task?</li>
    <li>A study of imperfect annotations: what is their impact on training deep networks, what learning strategies can we use when dealing with imperfections, what is the impact of the imperfections on evaluation metrics?</li>
    <li>An application of these concepts to the task of artefact detection and segmentation.</li>
    <li>A study of the impact of interobserver variability on the evaluation processes, and how to better include this variability into our evaluations.</li>
    <li>A description of some problems with quality control in digital pathology challenges, and recommendations on replicability and transparency for future challenges.</li>
</ul>

<p>The slideshow (and annotated slides) of the presentation are also available:</p>

<ul>
    <li><a href="./thesis/FOUCART_slideshow.ppsx">Slideshow [.ppsx]</a></li>
    <li><a href="./thesis/FOUCART_annotated_slides.pdf">Annotated slides [PDF]</a> (with the text corresponding more or less to what was said during the presentation - or at least what I planned to say!)</li>
</ul></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-10-19</pubDate>
    </item><item>
    <title>Is AI on the wrong path?</title>
    <link>https://research.adfoucart.be/ai-wrong-path</link>
    <description><p>A current trend in deep learning has been bothering me (and many other people, to be fair…). Deep learning has always been about creating bigger, more complex models, trained on more data, to get better results. But it seems to me that the trend is getting worse, and with big potential consequences for the future of AI research. It may just be in my head, but it seems pretty clear from where I’ve been standing in the field. Which is: slightly on the sidelines, as a researcher who works with deep learning, who regularly writes about deep learning, but who doesn’t really directly works on deep learning architectures.</p>

<p>I started my PhD back in 2015, right when TensorFlow was released and deep learning was just seemingly becoming accessible to all. State-of-the-art models could generally be trained from scratch in a day or so, on a (high-end) consumer-grade GPU. The combination of “Big Data” and powerful GPUs had fueled the shift to the deep learning paradigm, so it was natural that a lot of research came into pushing the boundaries of how large, how deep, how complex we could make those models.</p> 

<p>But my impression at the time was that there was also a strong concern for efficiency, and for keeping the complexity as low as possible [<a id='aiw-ref1-a' href='#aiw-ref1'>1</a>, <a id='aiw-ref2-a' href='#aiw-ref2'>2</a>]. This has always be, for me, a key design rule for a good model: it needs to be just complex enough to accurately represent the complexity of the distributions in the data. Moving beyond that complexity inevitably leads to overfitting. Another option for avoiding overfitting, of course, is to add more data. And today’s dominant trend seems to be: let’s be as complex as we can, and use pre-training on very large generic datasets (ImageNet, COCO, ADE20K, Cityscapes, Pascal VOC...) to get the required amount of data, before fine-tuning on whatever task we are actually trying to solve (if there is even one: it's often just about beating the benchmark).</p>

<p>I think that’s a worrying trend, for several reasons.</p>

<p>The first is: I’m not convinced it really works that well. It certainly does on tasks that require an extremely diverse set of features. Tasks like image generation and large language models are a good examples. Stable Diffusion, DALL-E, and all of the very impressive demos of huge transformer-based models are amazing at their task, but it sometimes seems that their main task is to make transformer-based models look amazing. Maybe that’s just because I generally work in a field with highly specialized tasks, such as “mitosis counting” or “tumour grading”, where it’s a lot more useful to combine very reliable models focused on very narrow, well-defined tasks rather than trying to have a model moderately successful at many different tasks. Transformers and pre-trained networks are widely used there as well, but they haven’t really demonstrated a clear benefit over well-designed, targeted models.</p>

<p>The second reason I don’t like this trend is that we are centralizing AI research into the hands of a few key players. These very large, very complex models require huge amount of resources, way beyond what’s possible with consumer-grade GPUs. That means that training those models is typically done on large clusters, powered by Amazon’s AWS, Microsoft’s Azure, or Google Cloud. Widespread use of transformers [<a id='aiw-ref3-a' href='#aiw-ref3'>3</a>] (or vision transformers, etc.) brings more clients to the big cloud providers. Who are, at the same time, driving a lot of the current research and setting the state-of-the-art. Transformers where introduced by a Google team. DALL-E is from OpenAI, with heavy links with Microsoft. Should we see a conspiracy by Google and Microsoft to drive AI research towards paradigms that allow them to centralize all AIs in their clouds? I don’t think it’s as intentional as that (and certainly some Google researchers like François Chollet are speaking out against the waste of resources [<a id='aiw-ref4-a' href='#aiw-ref4'>4</a>]). What I do think is that the focus on benchmarks such as ImageNet as a measure of the state-of-the-art has falsely given the impression that super-large models where the only way forward, and that there is no incentive for Google or Microsoft (or even Meta, who has plenty of computational resources available to them) to look much further than that.</p>

<blockquote>The extent to which some folks in deep learning research waste entire datacenter-months of computational resources to produce nothing but hot air is just so sad. It's like watching a macaque smear expensive oil paint on thousands of pristine linen canvases.
François Chollet [@fchollet on <a href="https://twitter.com/fchollet/status/1484044967907512323">Twitter</a>]
</blockquote> 

<p>The third and final reason is becoming even more obvious in the face of climate change and the current energy crisis: this is all very wasteful. I cannot see how using hundreds of thousands of GPU hours to find just the right hyperparameters that make a model gain a fraction of a percent on some general-purpose benchmark is a good use of our limited supplies of energy.</p>

<p>I understand the temptation to go big. Given what deep learning algorithms can do, it sometimes seems like “true” AI is right around the corner. If we just give it a bit more power, a bit more depth, a few billion parameters more... But I don’t think that’s going to work. And I don’t think that’s what we need AI to do right now.</p>

<p>In medical image analysis, the key challenge, in my opinion, is not in designing bigger, more complex networks, but in creating better datasets, clinically relevant tasks, and evaluation methods, in the absence of a clear, definitive ground truth (for more on that, see my PhD thesis [<a id='aiw-ref5-a' href='#aiw-ref5'>5</a>]!). Other fields will have their own, sometimes similar, challenges. Challenges that won’t be solved by blindly applying huge models, but by actually focusing on the practical applications of those models outside of controlled benchmarks.</p>

 
<h3>References</h3>

<ul type='1'>
    <li id='aiw-ref1'>[<a href='#aiw-ref1-a'>&#8593;</a>] Han et al., 2015. <a href="https://arxiv.org/abs/1510.00149">https://arxiv.org/abs/1510.00149</a></li>
    <li id='aiw-ref2'>[<a href='#aiw-ref2-a'>&#8593;</a>] Zela et al., 2018. <a href="https://arxiv.org/abs/1807.06906">https://arxiv.org/abs/1807.06906</a></li>
    <li id='aiw-ref3'>[<a href='#aiw-ref3-a'>&#8593;</a>] Vaswani et al., 2017. <a href="https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf">https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf</a></li>
    <li id='aiw-ref4'>[<a href='#aiw-ref4-a'>&#8593;</a>] F. Chollet on Twitter. <a href="https://twitter.com/fchollet/status/1484044967907512323">https://twitter.com/fchollet/status/1484044967907512323</a></li>
    <li id='aiw-ref5'>[<a href='#aiw-ref5-a'>&#8593;</a>] A. Foucart, 2022. <a href="https://research.adfoucart.be/thesis/FOUCART_Adrien_Dissertation.pdf">[<img src='pdf.png' width="20px" /> Dissertation]</a></li>
</ul></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-10-04</pubDate>
    </item><item>
    <title>A list of digital pathology image analysis challenges</title>
    <link>https://research.adfoucart.be/challenges</link>
    <description><p>There are several lists of digital pathology challenges and/or datasets floating around in different publications and, of course, on <a href="https://grand-challenge.org">grand-challenge.org</a>, but they never fit exactly what I'm looking for, and either miss some or include some that I would consider as slightly different modalities (such as cytology).</p>

<p>So here is the list I'm including in my thesis, as it may be useful to someone else. I included all challenges between 2010 and 2021 that I could find that used Whole Slide Images (WSIs) and/or image patches extracted from WSIs, either with H&E or IHC staining. I report here the reference to either the post-challenge publication if it exists, or the challenge website if it doesn't, and a (very short) description of the challenge's task(s).</p>

<table>
<thead>
<tr>
<th>Name, Year</th>
<th>Post-challenge publication or website</th>
<th>Task(s)</th>
</tr>
</thead>
<tbody>
<tr>
<td>PR in HIMA, 2010</td>
<td>Gurcan, 2010 [1]</td>
<td>Lymphocyte segmentation , centroblast detection.</td>
</tr>
<tr>
<td>MITOS, 2012</td>
<td>Roux, 2013 [2]</td>
<td>Mitosis detection.</td>
</tr>
<tr>
<td>AMIDA, 2013</td>
<td>Veta, 2015 [3]</td>
<td>Mitosis detection.</td>
</tr>
<tr>
<td>MITOS-ATYPIA, 2014</td>
<td><a href="https://mitos-atypia-14.grand-challenge.org/">Challenge website</a></td>
<td>Mitosis detection , nuclear atypia scoring.</td>
</tr>
<tr>
<td>Brain Tumour DP Challenge, 2014</td>
<td><a href="https://web.archive.org/web/20150222160406/http:/pais.bmi.stonybrookmedicine.edu/">Challenge website</a></td>
<td>Necrosis region segmentation , gliobastoma multiforme / low grade glioma classification.</td>
</tr>
<tr>
<td>Segmentation of Nuclei (SNI) in DP Images, 2015</td>
<td><a href="https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=20644646">Description in TCIA wiki</a></td>
<td>Nuclei segmentation.</td>
</tr>
<tr>
<td>BIOIMAGING, 2015</td>
<td><a href="http://www.bioimaging2015.ineb.up.pt/challenge_overview.html">Challenge website</a></td>
<td>Tumour classification.</td>
</tr>
<tr>
<td>GlaS, 2015</td>
<td>Sirinukunwattana, 2017 [4]</td>
<td>Gland segmentation.</td>
</tr>
<tr>
<td>TUPAC, 2016</td>
<td>Veta, 2019 [5]</td>
<td>Mitotic scoring , PAM50 scoring , mitosis detection.</td>
</tr>
<tr>
<td>CAMELYON, 2016</td>
<td>Ehteshami Bejnordi, 2017 [6]</td>
<td>Metastases detection.</td>
</tr>
<tr>
<td>SNI, 2016</td>
<td><a href="http://web.archive.org/web/20161116133639/http:/miccai.cloudapp.net/competitions/45">Challenge website</a></td>
<td>Nuclei segmentation.</td>
</tr>
<tr>
<td>HER2, 2016</td>
<td>Qaiser, 2018 [7]</td>
<td>HER2 scoring.</td>
</tr>
<tr>
<td>Tissue MicroarrayAnalysis in ThyroidCancer Diagnosis, 2017</td>
<td>Wang, 2018 [8]</td>
<td>Prediction of BRAF gene mutation (classification), TNM stage (scoring), extension status (scoring), tumour size (regression), metastasis status (scoring).</td>
</tr>
<tr>
<td>CAMELYON, 2017</td>
<td>Bandi, 2019 [9]</td>
<td>Tumour scoring (pN-stage) in lymph nodes.</td>
</tr>
<tr>
<td>SNI, 2017</td>
<td>Vu, 2019 [10]</td>
<td>Nuclei segmentation.</td>
</tr>
<tr>
<td>SNI, 2018</td>
<td>Kurc, 2020 [11]</td>
<td>Nuclei segmentation.</td>
</tr>
<tr>
<td>ICIAR BACH, 2018</td>
<td>Aresta, 2019 [12]</td>
<td>Tumour type patch classification , tumour type segmentation.</td>
</tr>
<tr>
<td>MoNuSeg, 2018</td>
<td>Kumar, 2020 [13]</td>
<td>Nuclei segmentation.</td>
</tr>
<tr>
<td>C-NMC, 2019</td>
<td>Gupta, 2019 [14]</td>
<td>Normal/Malignant cell classification.</td>
</tr>
<tr>
<td>BreastPathQ, 2019</td>
<td>Petrick, 2021 [15]</td>
<td>Tumour cellularity assessment (regression).</td>
</tr>
<tr>
<td>PatchCamelyon, 2019</td>
<td><a href="https://github.com/basveeling/pcam">Challenge website</a></td>
<td>Metastasis patch classification.</td>
</tr>
<tr>
<td>ACDC@LungHP, 2019</td>
<td>Li, 2019 [16]</td>
<td>Lung carcinoma segmentation.</td>
</tr>
<tr>
<td>LYON, 2019</td>
<td>Swiderska-Chadaj, 2019 [17]</td>
<td>Lymphocyte detection.</td>
</tr>
<tr>
<td>PAIP, 2019</td>
<td>Kim, 2021 [18]</td>
<td>Tumour segmentation , viable tumour ratio estimation (regression).</td>
</tr>
<tr>
<td>Gleason, 2019</td>
<td><a href="https://gleason2019.grand-challenge.org/">Challenge website</a></td>
<td>Tumour scoring , Gleason pattern region segmentation.</td>
</tr>
<tr>
<td>DigestPath, 2019</td>
<td>Zhu, 2021 [19]</td>
<td>Signet ring cell detection , lesion segmentation , benign/malign tissue classification.</td>
</tr>
<tr>
<td>LYSTO, 2019</td>
<td><a href="https://lysto.grand-challenge.org/LYSTO/">Challenge website</a></td>
<td>Lymphocyte counting.</td>
</tr>
<tr>
<td>BCSS, 2019</td>
<td>Amgad, 2019 [20]</td>
<td>Breast cancer regions semantic segmentation.</td>
</tr>
<tr>
<td>ANHIR, 2019</td>
<td>Borovec, 2020 [21]</td>
<td>WSI registration.</td>
</tr>
<tr>
<td>HeroHE, 2020</td>
<td>Conde-Sousa, 2021 [22]</td>
<td>HER2 scoring.</td>
</tr>
<tr>
<td>MoNuSAC, 2020</td>
<td>Verma, 2021 [23]</td>
<td>Nuclei detection , segmentation, and classification.</td>
</tr>
<tr>
<td>PANDA, 2020</td>
<td>Bulten, 2022 [24]</td>
<td>Prostate cancer Gleason scoring.</td>
</tr>
<tr>
<td>PAIP, 2020</td>
<td><a href="https://paip2020.grand-challenge.org/Workshop/">Challenge website</a></td>
<td>Colorectal cancer MSI scoring and whole tumour area segmentation.</td>
</tr>
<tr>
<td>Seg-PC, 2021</td>
<td><a href="https://segpc-2021.grand-challenge.org/SegPC-2021/">Challenge website</a></td>
<td>Multiple myeloma plasma cells segmentation.</td>
</tr>
<tr>
<td>PAIP, 2021</td>
<td><a href="https://paip2021.grand-challenge.org/Workshop/">Challenge website</a></td>
<td>Perineural invasion detection and segmentation.</td>
</tr>
<tr>
<td>NuCLS, 2021</td>
<td>Amgad, 2021 [25]</td>
<td>Nuclei detection , segmentation and classification.</td>
</tr>
<tr>
<td>WSSS4LUAD, 2021</td>
<td><a href="https://wsss4luad.grand-challenge.org/">Challenge website</a></td>
<td>Tissue semantic segmentation from weak, image-level annotations.</td>
</tr>
<tr>
<td>MIDOG, 2021</td>
<td><a href="https://midog2021.grand-challenge.org/">Challenge website</a></td>
<td>Mitosis detection.</td>
</tr>
</tbody>
</table>

<p>[1] M. N. Gurcan, A. Madabhushi, and N. Rajpoot, &quot;Pattern Recognition in Histopathological Images: An ICPR 2010 Contest,&quot; in Lecture Notes in Computer Science, vol. 6388, 2010, pp. 226–234.</p>
<p>[2] L. Roux et al., &quot;Mitosis detection in breast cancer histological images An ICPR 2012 contest,&quot; J. Pathol. Inform., vol. 4, no. 1, p. 8, 2013, doi: 10.4103/2153-3539.112693.</p>
<p>[3] M. Veta et al., &quot;Assessment of algorithms for mitosis detection in breast cancer histopathology images,&quot; Med. Image Anal., vol. 20, no. 1, pp. 237–248, Feb. 2015, doi: 10.1016/j.media.2014.11.010.</p>
<p>[4] K. Sirinukunwattana, J. P. W. Pluim, H. Chen, and Others, &quot;Gland segmentation in colon histology images: The glas challenge contest,&quot; Med. Image Anal., vol. 35, pp. 489–502, 2017, doi: 10.1016/j.media.2016.08.008.</p>
<p>[5] M. Veta et al., &quot;Predicting breast tumor proliferation from whole-slide images: The TUPAC16 challenge,&quot; Med. Image Anal., vol. 54, pp. 111–121, May 2019, doi: 10.1016/j.media.2019.02.012.</p>
<p>[6] B. Ehteshami Bejnordi et al., &quot;Diagnostic Assessment of Deep Learning Algorithms for Detection of Lymph Node Metastases in Women With Breast Cancer,&quot; JAMA, vol. 318, no. 22, p. 2199, Dec. 2017, doi: 10.1001/jama.2017.14585.</p>
<p>[7] T. Qaiser et al., &quot;HER2 challenge contest: a detailed assessment of automated HER2 scoring algorithms in whole slide images of breast cancer tissues,&quot; Histopathology, vol. 72, no. 2, pp. 227–238, Jan. 2018, doi: 10.1111/his.13333.</p>
<p>[8] C.-W. Wang et al., &quot;A benchmark for comparing precision medicine methods in thyroid cancer diagnosis using tissue microarrays,&quot; Bioinformatics, vol. 34, no. 10, pp. 1767–1773, May 2018, doi: 10.1093/bioinformatics/btx838.</p>
<p>[9] P. Bandi et al., &quot;From Detection of Individual Metastases to Classification of Lymph Node Status at the Patient Level: The CAMELYON17 Challenge,&quot; IEEE Trans. Med. Imaging, vol. 38, no. 2, pp. 550–560, Feb. 2019, doi: 10.1109/TMI.2018.2867350.</p>
<p>[10] Q. D. Vu et al., &quot;Methods for Segmentation and Classification of Digital Microscopy Tissue Images,&quot; Front. Bioeng. Biotechnol., vol. 7, Apr. 2019, doi: 10.3389/fbioe.2019.00053.</p>
<p>[11] T. Kurc et al., &quot;Segmentation and Classification in Digital Pathology for Glioma Research: Challenges and Deep Learning Approaches,&quot; Front. Neurosci., vol. 14, Feb. 2020, doi: 10.3389/fnins.2020.00027.</p>
<p>[12] G. Aresta et al., &quot;BACH: Grand challenge on breast cancer histology images,&quot; Med. Image Anal., vol. 56, pp. 122–139, 2019, doi: 10.1016/j.media.2019.05.010.</p>
<p>[13] N. Kumar et al., &quot;A Multi-Organ Nucleus Segmentation Challenge,&quot; IEEE Trans. Med. Imaging, vol. 39, no. 5, pp. 1380–1391, 2020, doi: 10.1109/TMI.2019.2947628.</p>
<p>[14] A. Gupta and R. Gupta, Eds., ISBI 2019 C-NMC Challenge: Classification in Cancer Cell Imaging. Singapore: Springer Singapore, 2019.</p>
<p>[15] N. Petrick et al., &quot;SPIE-AAPM-NCI BreastPathQ challenge: an image analysis challenge for quantitative tumor cellularity assessment in breast cancer histology images following neoadjuvant treatment,&quot; J. Med. Imaging, vol. 8, no. 03, May 2021, doi: 10.1117/1.JMI.8.3.034501.</p>
<p>[16] Z. Li et al., &quot;Deep Learning Methods for Lung Cancer Segmentation in Whole-Slide Histopathology Images - The ACDC@LungHP Challenge 2019,&quot; IEEE J. Biomed. Heal. Informatics, vol. 25, no. 2, pp. 429–440, 2021, doi: 10.1109/JBHI.2020.3039741.</p>
<p>[17] Z. Swiderska-Chadaj et al., &quot;Learning to detect lymphocytes in immunohistochemistry with deep learning,&quot; Med. Image Anal., vol. 58, p. 101547, Dec. 2019, doi: 10.1016/j.media.2019.101547.</p>
<p>[18] Y. J. Kim et al., &quot;PAIP 2019: Liver cancer segmentation challenge,&quot; Med. Image Anal., vol. 67, p. 101854, 2021, doi: 10.1016/j.media.2020.101854.</p>
<p>[19] C. Zhu et al., &quot;Multi-level colonoscopy malignant tissue detection with adversarial CAC-UNet,&quot; Neurocomputing, vol. 438, pp. 165–183, May 2021, doi: 10.1016/j.neucom.2020.04.154.</p>
<p>[20] M. Amgad et al., &quot;Structured crowdsourcing enables convolutional segmentation of histology images,&quot; Bioinformatics, vol. 35, no. 18, pp. 3461–3467, 2019, doi: 10.1093/bioinformatics/btz083.</p>
<p>[21] J. Borovec et al., &quot;ANHIR: Automatic Non-Rigid Histological Image Registration Challenge,&quot; IEEE Trans. Med. Imaging, vol. 39, no. 10, pp. 3042–3052, Oct. 2020, doi: 10.1109/TMI.2020.2986331.</p>
<p>[22] E. Conde-Sousa et al., &quot;HEROHE Challenge: assessing HER2 status in breast cancer without immunohistochemistry or in situ hybridization,&quot; Nov. 2021.</p>
<p>[23] R. Verma et al., &quot;MoNuSAC2020: A Multi-Organ Nuclei Segmentation and Classification Challenge,&quot; IEEE Trans. Med. Imaging, vol. 40, no. 12, pp. 3413–3423, Dec. 2021, doi: 10.1109/TMI.2021.3085712.</p>
<p>[24] W. Bulten et al., &quot;Artificial intelligence for diagnosis and Gleason grading of prostate cancer: the PANDA challenge,&quot; Nat. Med., vol. 28, no. 1, pp. 154–163, Jan. 2022, doi: 10.1038/s41591-021-01620-2.</p>
<p>[25] M. Amgad et al., &quot;NuCLS: A scalable crowdsourcing, deep learning approach and dataset for nucleus classification, localization and segmentation,&quot; no. Cche 57357, pp. 1–45, 2021.</p>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-04-26</pubDate>
    </item><item>
    <title>Final comments on the results of the MoNuSAC challenge</title>
    <link>https://research.adfoucart.be/monusac-error</link>
    <description><p>Our <a href="https://ieeexplore.ieee.org/document/9745980">comment article</a> Comments on "MoNuSAC2020: A Multi-Organ Nuclei Segmentation and Classification Challenge" was just published in the April 2022 issue of the IEEE Transactions on Medical Imaging, alongside the <a href="https://ieeexplore.ieee.org/document/9745890">author's reply</a>. The whole story is, in my opinion, a very interesting example of things that can go wrong with digital pathology challenges, and of some weaknesses of the scientific publication industry. But let's start from the beginning...</p>


  <img src='./img/MoNuSAC.png' class="img-rounded img-responsive" alt="MoNuSAC 2020 Banner" />


<h3>Summary of previous events</h3>

<p>MoNuSAC was a nuclei detection, segmentation and classification challenge hosted at the ISBI 2020 conference. The challenge results were <a href="https://monusac-2020.grand-challenge.org/Results/">posted online</a>, and a post-challenge paper was published in the IEEE Transactions on Medical Imaging [<a id='me-ref1-a' href='#me-ref1'>1</a>] (available in "Early Access" on June 4th, 2021, and published in December 2021). Challenge organisers also published <a href="https://monusac-2020.grand-challenge.org/Data/">ground truth annotations</a> for the test set, as well as a "colour-coded predictions" on the test set of four of the top-five teams (the link for the L1 team pointing to the predictions of the PL2 team), and the <a href="https://github.com/ruchikaverma-iitg/moNuSAC">evaluation code</a> used to score the participants' submissions.</p>

<p>In September 2021, as I was working on an analysis of the Panoptic Quality metric [<a id='me-ref2-a' href='#me-ref2'>2</a>] used in the challenge, I discovered a bug in the evaluation code and alerted the organisers on September 20th, 2021. They initially replied that "[t]he code to compute PQ is correct". After verification, I confirmed the error and sent a Jupyter Notebook <a href="./monusac/pq_error.html">demonstrating it in action</a>.</p>

<p>As I was trying to assess the potential effects of the error on the challenge results, I discovered several additional issues in the evaluation process. On September 22, 2021, I sent a <a href="./monusac/detailed_report.pdf">detailed report</a> to the organisers explaining the different problems and offering to "collaborate with [them] in making a correction to the published results". I received no response.</p>

<p>On October 12th, 2021, my PhD supervisor Prof. Decaestecker contacted the managing editor of the IEEE Transactions on Medical Imaging to ask what the procedure was for submitting "comment articles" to the journal, as it seemed to be the preferred method in IEEE journals for reporting potential errors in published papers. After some back-and-forth to clarify the procedure, we submitted our comment article on October 20th, 2021. The comment article was transmitted to the original authors. On February 23rd, 2022, we received notice from the editor-in-chief that our comment article was accepted. Both our comments and a response by the original authors were finally published in the April 2022 issue of the journal [<a id='me-ref3-a' href='#me-ref3'>3</a>, <a id='me-ref4-a' href='#me-ref4'>4</a>]... which brings us back to the present.</p>

<h3>Claims and responses</h3>

<p>Four main issues were raised in our original comment. In this section, I would like to go through our claims, the response from Verma et al., and my thoughts on that response. The four issues that we raised are:</p>

<ul>
    <li>There was a typo in the code used to compute the PQ metric, which led to False Positives being incorrectly added to the count.</li>
    <li>There was a confusion between two classes in the detailed per-organ and per-class results, where Macrophage results were reported as Neutrophils and vice-versa.</li>
    <li>Because of the way the code processed prediction files, when where there were no ground truth objects of a given class for an image, the predicted objects of that class were not taken into account as False Positives.</li>
    <li>The aggregation process of the metric was implemented by computing the PQ metric on each "image patch" and then computing the mean, while the published methodology implied that the aggregation would take place at the "patient" level (which would make a lot more sense given the disparity of image sizes and numbers of objects).</li>
</ul>

<p>Let's take them one by one.</p>

Computation of the PQ

<p>This one is very straightforward. In their reply, the authors recognised the error and recomputed the results after correction. Fortunately, they find that "the impact of fixing this bug is small". The increase of .3%-.5% in the overall PQ for most teams with the corrected code [<a id='me-ref4-a' href='#me-ref4'>4</a>, Table I] is in line with our own experiments. They also find, as we suspected, that some teams were disproportionately affected ("L8" moves three ranks up to "L5" with a 16% improvement in PQ, "L13" moves three ranks up to "L10" with a 16.5% improvement): this is because the error is particularly strong if the object labels between the "ground truth" and "predictions" are very different (as in: using different ranges of numbers), so it's likely that these teams used a different labelling process (full explanation in our comments [<a id='me-ref3-a' href='#me-ref3'>3</a>] or on our <a href="https://github.com/adfoucart/monusac-results-code-analysis/blob/main/MoNuSAC%20challenge%20results%20analysis.ipynb">Notebook on GitHub</a>).</p>

Class confusion

<p>Here, the authors also admit the error, and issue an amended table for the supplementary materials, so I have no further comments to make.</p>

Missing false positives

<p>To our third claim, the authors argue that it comes down to a matter of methodological choice. Quoting from their reply [<a id='me-ref4-a' href='#me-ref4'>4</a>, II.C]:</p>

<blockquote>
[T]he false positive for one class will be false negative for another class in an mutually exclusive and exhaustive multi-class multi-instance detection problem. We did not want to double count an error, and therefore our loops for error counting run over the ground truth objects. The interpretation of positives and negatives in multi-class problems is a matter interpretation until settled, and this leads to multiple ways of computing the PQ metric for multi-class problems.
</blockquote>

<p>I disagree with this for several reasons. To better explain what's going on (the problem, the author's response, and why I think it's wrong), it will be easier to look at a fabricated example. In the figure below, we have a "ground truth" and a "prediction", where colours correspond to classes, numbers to ground truth instance labels, and letters to predicted instance labels.</p>


  <img src='./img/cm_example_label.png' class="img-rounded img-responsive" alt="Fabricated example of missing false positive case" width="400px" />


<p>The submission format require each class' labels to be in a separate file, so in such a situation we would have two "ground truth" label maps (let's call them GT_Blue and GT_Green) and three "prediction" label maps (P_Blue, P_Green and P_White), each containing the labelled mask for one specific class. A pixel cannot belong to two different classes (although there is no mechanism in the code to verify that the submission is valid in this regard).</p>

<p>In the current implementation of the evaluation code, these files would be processed in the following way:</p>

<pre>
    Open GT_Blue and get label map.
    Open P_Blue to get corresponding predictions.
    Compute PQ_Blue based on GT_Blue and P_Blue. 
        -> 2 True Positives (1-A, 4-D) and 1 False Positive (C)
    Open GT_Green and get label map.
    Open P_Green to get corresponding predictions.
    Compute PQ_Green. 
        -> 1 True Positive (5-E) and 2 False Negatives (2, 3)
    Stop.
</pre>

<p>So P_White is never opened, and the False Positive "B" will never be counted. If we look at the full confusion matrix for this image, we should have (with rows corresponding to "ground truth" classes and columns to "predicted" classes:</p>

<table style="text-align: center;">
    <tr>
        <th></th><th>No obj</th><th>Blue</th><th>Green</th><th>White</th>
    </tr>
    <tr>
        <th>No obj</th><td>0</td><td>0</td><td>0</td><td>1</td>
    </tr>
    <tr>
        <th>Blue</th><td>0</td><td>2</td><td>0</td><td>0</td>
    </tr>
    <tr>
        <th>Green</th><td>1</td><td>1</td><td>1</td><td>0</td>
    </tr>
    <tr>
        <th>White</th><td>0</td><td>0</td><td>0</td><td>0</td>
    </tr>
</table>

<p>But with the evaluation code as it is written, we end up with:</p>

<table style="text-align: center;">
    <tr>
        <th></th><th>No obj</th><th>Blue</th><th>Green</th><th>White</th>
    </tr>
    <tr>
        <th>No obj</th><td>0</td><td>0</td><td>0</td><td bgcolor=red>0</td>
    </tr>
    <tr>
        <th>Blue</th><td>0</td><td>2</td><td>0</td><td>0</td>
    </tr>
    <tr>
        <th>Green</th><td>1</td><td>1</td><td>1</td><td>0</td>
    </tr>
    <tr>
        <th>White</th><td>0</td><td>0</td><td>0</td><td>0</td>
    </tr>
</table>

<p>Coming back now to the author's reply, the statement that "the false positive for one class will be false negative for another" is incorrect... unless you count the background "no object" class, which we shouldn't, as the PQ is not computed for the background class. In our fabricated example, the false positive will not be counted at all.</p>

<p>What if the white "B" object had been matched with, for instance, the green "2" object, which at the moment is a "Green False Negative"? Then the "2-B" match would be both a "Green FN" and a "White FP". The idea that, in such case, it should only be counted once, however, would be an important deviation the PQ metric as defined in [<a id='me-ref5-a' href='#me-ref5'>5</a>], and it should then be clearly explained in the methods.</p>

<p>As the metric is computed independently for each class, the misclassification should clearly impact both the "Green" class metric and the "White" class metric. In fact, it is the case for other misclassifications, such as the "3-C" match in our fabricated example, which is counted both as a "Green FN" and a "Blue FP". The only time such misclassifications are not counted "twice" in the evaluation code is if there is no ground truth instance of that class in that particular image patch.</p>

<p>This is not a matter of interpretation in this case: it is clearly a mistake. The effect of that mistake can also be very large on the metric. From our experiment on the teams' released "colour-coded" predictions, correcting this mistake led to a drop of 13-15% in the computed PQ. For instance, looking at the SJTU_426 team, we have a 0.4% increase in the PQ when correcting the PQ computation typo (same as the challenge organisers), but we have a massive 14% drop in PQ when we add the missed False Positives:</p>

<table>
    <tr>
        <th>SJTU_426</th><th>Challenge eval.</th><th>Corr. Typo</th><th>Corr. FP</th><th>Corr. FP + Typo</th>
    </tr>
    <tr>
        <th>Verma et al.</th><td>0.579</td><td>0.618</td><td>-</td><td>-</td>
    </tr>
    <tr>
        <th>Recomputed</th><td>0.554</td><td>0.594</td><td>0.424</td><td>0.454</td>
    </tr>
</table>

<p>Our replication of the results are based on the "colour-coded" prediction maps. They will be slightly different from the original submission, but should be almost identical in terms of detection performances, which is what changes between those versions of the evaluation code. So while the absolute value is not exactly the same, the delta between the different error corrections should be in the right range.</p>

<p>There is an actual question of interpretation on how exactly those specific false positives should be counted. In our fabricated example, if there was no "White" prediction, then the PQ_white would be undetermined and wouldn't be counted in the average, but if we have a single false positive, we immediately get a PQ of 0 (hence the large impact on the metric). The solution to that problem, however, isn't to just remove the false positives... But to correct the last problem that we have to address: the aggregation process.</p>

Aggregation: patient vs patch

<p>There are two parts to the author's reply to our concerns about the aggregation process. The first part is, again, to argue that aggregating "per-patient" (i.e. computing the PQ directly at the patient level and then averaging over the 25 patients of the test set) or "per-patch" (computing the PQ at the image patch level and then averaging over the 101 image patches of the test set) is a methodological choice, and the second is to state that it was clear in their methodology that they chose the second option, so it's not an error on their part that requires a correction but rather a choice that may be discussed in future works.</p>

<p>Concerning the latter, there may indeed have been some confusion which, as they admit, comes from a typing error in their manuscript (where they mention "25 test images" and aggregating "25 PQ_i scores", which correspond to the number of patients and not the number of patches). It led me to the impression that there was simply a certain inconsistency in their naming convention through the paper, where "image" sometimes referred to WSI and sometimes to image patch.</p>

<p>Part of the confusion also comes from their relatively consistent use of the term "sub-image" in the <a href="https://doi.org/10.13140/RG.2.2.12290.02244/1">pre-challenge description of the dataset</a> to refer to the image patches. I was regularly consulting that document as well during my analysis, so it made sense to me that "image" would in general refer to "Whole-Slide Image" and "sub-image" to "image patch". As the information in the <a href="https://drive.google.com/file/d/1kdOl3s6uQBRv0nToSIf1dPuceZunzL4N/view">supplementary materials</a> shows there is only one WSI per patient, this interpretation seemed reasonable.</p>

<p>I can certainly believe that it wasn't their intent, however, and that it was very clear for them (and possibly for the challenge participants) that the PQ was always meant to be computed per-patch in the challenge. So the question becomes: is that a valid methodological choice?</p>

<p>First: what is the problem with computing the PQ "per-patch"? The difference in size between the different patches is enormous, with the smallest image in the dataset having a size of 86x33px, and the largest a size of 1760x1771px, more than a 1000 times larger in number of pixels! The difference in terms of number of ground truth objects is also very large, ranging from 2 to 861 per patch. Even looking at objects of the least frequent class, we can have anywhere between 1 and 15 instances in patches where the class is represented. This means that the cost of making an error on some image patches is orders of magnitudes higher than on others.</p>

<p>The better option that I identified in our comments article was to compute the PQ per-patient. This vastly decreases the size variability, and it also makes sense for a biomedical dataset. Patches taken from the same patient (and particularly, as is the case here, the same WSI) may share some common properties, so that they can't really be considered independent samples. It also increases the number of ground truth objects at the moment where the metric is computed, which avoids the effect of a single False Positive for an object not present in the ground truth immediately setting the PQ to 0, as in our previous example. The distribution of ground truth objects is more likely to be representative on a larger sample, and the PQ will therefore be more less "noisy". Being able to estimate whether a particular algorithm's performances are steady over multiple patients is also very valuable information, so it makes sense to choose that over the other option identified in the author's reply, which is to compute the PQ just once, at the level of the entire dataset.</p>

<p>The main drawback that the authors identify to using the "one PQ on the whole dataset" approach in their reply is that "this approach does not allow for a robust computation of confidence intervals". I'm very unconvinced by this line of reasoning. First of all, it doesn't really say anything about the per-patient proposition (which does allow for confidence intervals, although obviously with less samples these will be larger). Second, there is the aforementioned problem of the samples' independence. Confidence intervals should really be measured at the patient level anyway, as the patient are, in the end, the "samples" that we are studying.</p>

<p>If this is a methodological question, I don't really think it's as open as the author's reply would suggest. In this case, it seems very clear that the per-patient option is better, and I would even say that, given the scale of the "weights" difference given to the errors, the "per-patch" option is in this case even incorrect. It could be justified when the extracted patch are of very similar sizes, but not in this particular dataset.</p>

<h3>Last thoughts on this whole story</h3>

<p>The corrected results published by Verma et al. in [<a id='me-ref4-a' href='#me-ref4'>4</a>] are, in my opinion, still incorrect. The correct ranking of the challenge is, therefore, unknown, and we shouldn't draw any conclusions on the merits of the different methods proposed by the participants. I doubt that the ranking would change by a lot if all corrections were implemented, but as we have seen with the first correction, it's possible that some participants were affected by some of the errors more than others.</p>

<p>While I'm disappointed by the unwillingness of the organisers to directly work with us on correcting the problems, I would like to emphasise that I don't think they did a bad job overall with the challenge. In fact, as I've repeatedly highlighted, their level of transparency on their data and code is higher than any other digital pathology segmentation challenge that I've seen. That's what interested me in their data in the first place, and what allowed us to perform our study on the PQ metric [<a id='me-ref2-a' href='#me-ref2'>2</a>].</p>

<p>Organising a successful international competition and compiling such a dataset is a very big achievement, whatever problems may remain in the results. This whole saga also highlight how important it is to increase the transparency in the reporting of challenge results in general. Without the team's predictions and the evaluation code, results are unverifiable and unreplicable. MoNuSAC really went in the right direction here, although they fell short of releasing all participants' results, and released a "visualisation" instead of the raw prediction maps.</p>

<p>What we can see in every challenge that we analyse is that mistakes happen all the time: in the dataset production, in the evaluation methodology, in the code... Challenges are extremely costly and time-consuming to organise, and they use up a lot of resources (from the organisers, but also from the participants). We should really try to make sure that the results that we get from them are accurate. If challenge organisers go for a fully transparent approach to the evaluation process, then the responsibility of checking the validity of the results becomes shared by the whole community, and our trust in these results is improved.</p>

<p>There are also some things in the process with the journal which I think could be improved. First of all, given the nature of the error we found, I think that some form of notice should have been quickly added to the original publication on the journal's website. Between October 2021 and February 2022, there was reasonable doubt about the validity of the results and, since February 2022 and the acceptation of the authors' reply, it is certain that the original results are incorrect.</p>

<p>Even now that the comment and response have been published, there is still no link to those from the original publication, meaning that the original results are likely to be cited without noticing that they have been amended, and that there are additional concerns beyond what was corrected.</p>

<p>I do think that the journal's Editor-in-Chief and Managing Editor were reactive and tried to do things right. The fact that they had to navigate through a process that seemed obscure to everyone is worrying, however. In many ways, "peer-review" starts when the article is published and, therefore, actually available to more than a couple of "peers". Corrections and retractions should be a natural part of the publishing process. Mistakes happen. They only are a problem if they can't be detected and corrected.</p>

<h3>References</h3>

<ul type='1'>
    <li id='me-ref1'>[<a href='#me-ref1-a'>&#8593;</a>] R. Verma et al. MoNuSAC2020: A Multi-Organ Nuclei Segmentation and Classification Challenge. IEEE Transactions on Medical Imaging,  2021. <a href="https://doi.org/10.1109/TMI.2021.3085712">doi:10.1109/TMI.2021.3085712</a></li>
    <li id='me-ref2'>[<a href='#me-ref2-a'>&#8593;</a>] A. Foucart et al. Evaluating participating methods in image analysis challenges: lessons from MoNuSAC 2020. Preprint under review,  2022. <a href="https://doi.org/10.13140/RG.2.2.11627.00801">doi:10.13140/RG.2.2.11627.00801</a> <a href="./preprint/FOUCART22-MoNuSAC.pdf"><img src='pdf.png' width="20px" /> Download</a></li>
    <li id='me-ref3'>[<a href='#me-ref3-a'>&#8593;</a>] A. Foucart et al. Comments on "Monusac2020: A Multi-Organ Nuclei Segmentation and Classification Challenge". IEEE Transactions on Medical Imaging, 2022. <a href="https://doi.org/10.1109/TMI.2022.3156023">doi:10.1109/TMI.2022.3156023</a>.</li>
    <li id='me-ref4'>[<a href='#me-ref4-a'>&#8593;</a>] R. Verma et al. Author's Reply to "Monusac2020: A Multi-Organ Nuclei Segmentation and Classification Challenge". IEEE Transactions on Medical Imaging, 2022. <a href="https://doi.org/10.1109/TMI.2022.3157048">doi:10.1109/TMI.2022.3157048</a></li>
    <li id='me-ref5'>[<a href='#me-ref5-a'>&#8593;</a>] A. Kirillov et al. Panoptic Segmentation. CVPR, 2019. <a href="https://doi.org/10.1109/CVPR.2019.00963">doi:10.1109/CVPR.2019.00963</a></li>
</ul></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2022-04-05</pubDate>
    </item><item>
    <title>Deep Learning for Image Analysis: video tutorial</title>
    <link>https://research.adfoucart.be/dlia-video</link>
    <description>

<p>Last year, I did a video tutorial for the INFO-H-501 "Pattern Recognition and Image Analysis" course at the Université Libre de Bruxelles. I apparently forgot to also put it here, so here we are...</p>

<p>The video contains a bit of theory on how to approach a segmentation problem with "Deep Learning", and a guide to a working implementation of the pipeline using Tensorflow v2.</p>

<p>The dataset used in the video is the publicly released <a href="https://warwick.ac.uk/fac/cross_fac/tia/data/glascontest/">GlaS challenge dataset</a>. All the code can be found here on GitHub: <a href="https://github.com/adfoucart/dlia-videos">https://github.com/adfoucart/dlia-videos</a>.</p></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2021-09-21</pubDate>
    </item><item>
    <title>Computers in pathology: a long history</title>
    <link>https://research.adfoucart.be/computers-pathology</link>
    <description><blockquote>We (...) have an unusual situation that we wish to search for objects present in low concentration, the quantity of which is irrelevant, and the final classification of which is not agreed by different experts. At this point the engineer may well begin to feel despair. (A.I. Spriggs, 1969 [<a id='cpath-ref1-a' href='#cpath-ref1'>1</a>])</blockquote>

<p>This quote comes from a 1969 publication entitled "Automatic scanning for cervical smears". This paper is interesting for several reasons:</p>

<ol type="a">
	<li>It shows that "computer-assisted pathology" is not a new idea.</li>
	<li>It identifies difficulties in designing such systems which are still very relevant today.</li>
</ol>

<p>Specifically, Spriggs worries that the "Carcinoma in situ" which they are trying to detect "is not a clear-cut entity", with "a whole spectrum of changes" where "nobody knows where to draw the line and when to call the lesion definitely precancerous". To sum up: "We therefore do not really know which cases we wish to find". Moreover, "the opinions of different observers also vary".</p>

<p>On that latter point, Spriggs also notes that the classifications or grading used by pathologists are often more "degrees of confidence felt by the observer" than measurable properties of the cells. "It is therefore nonsensical to specify for a machine that it should identify these classes." The only reason to do it is that we have to measure the performance of the machine against the opinion of the expert. This is slightly less of a problem today, as the grading systems have evolved and become more focused on quantifiable measurements, but they still often allow for large margins of interpretation. As we match our systems against these grading, we constrain them to "mimic" the reasoning of the pathologists instead of focusing on the underlying problem of finding relationships between the images and the evolution of the disease itself.</p>

<p>"Digital pathology" really started to appear in the scientific literature around the year 2000. At that time, the focus was on telepathology or virtual microscopy: allowing pathologists to move away from the microscope, to more easily share images for second opinions, and to better integrate the image information with the rest of the patient's record [<a id='cpath-ref2-a' href='#cpath-ref2'>2</a>, <a id='cpath-ref3-a' href='#cpath-ref3'>3</a>, <a id='cpath-ref4-a' href='#cpath-ref4'>4</a>].</p>

<p>Digital pathology also relates to image analysis. The 2014 Springer book "Digital Pathology" [<a id='cpath-ref5-a' href='#cpath-ref5'>5</a>], for instance, includes in their definition not only the acquisition of the specimens "in digital form", but also "their real-time evaluation", "data mining", and the "development of artificial intelligence tools".</p>

<p>The terminology may be recent, but the core idea behind it (linking an acquisition device to a computer to automate the analysis of a pathology sample) is about as old as computers. One of the earliest documented attempts may be the Cytoanalyzer... in the 1950s.</p>


  <a href='./img/life-cytoanalyzer.jpg'><img src='./img/life-cytoanalyzer.jpg' class="img-rounded img-responsive" alt="" width='500px' /></a>
  Fig. 1 - LIFE Magazine article on the "Cytoanalyzer", April 25th, 1955 (<a href="https://books.google.be/books?id=elYEAAAAMBAJ&printsec=frontcover&hl=nl&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false">Google Books</a>).


<p>Two paragraphs in a 1955 issue of LIFE magazine, sandwiched between ads for mattresses and cars, presents an "electronic gadget" which "will spot abnormal cells (...) in about a minute, saving time-consuming specialized analysis for every case". A more comprehensive description of the prototype can be found by Walter E. Tolles in the Transactions of the New York Academy of Science [<a id='cpath-ref6-a' href='#cpath-ref6'>6</a>].</p>

<p>The Cytoanalyzer (see Fig. 2) had three units: the power supply and computer (left), the scanner (middle) and the oscilloscopes for monitoring and presentation (right). The scanner converts the "density field" of the slide into an electric current, which is used to analyse the properties of the cells and discriminate between normal and abnormal ones.</p>


  <a href='./img/tolles-cytoanalyzer.jpg'><img src='./img/tolles-cytoanalyzer.jpg' class="img-rounded img-responsive" alt="" width='500px' /></a>
  Fig 2 - 1955 version of the cytoanalyzer, from [<a id='cpath-ref6-a' href='#cpath-ref6'>6</a>].


<p>Two clinical trials of the Cytoanalyer were conducted, in 1958-59 and in 1959-60. An analysis of the results by Spencer & Bostrom in 1962 [<a id='cpath-ref7-a' href='#cpath-ref7'>7</a>] considered its results to be "inadequate for practical application of the instrument".</p>

<p>Decades passed. Hardware and software vastly improved, yet even as new methods in image analysis and artificial intelligence got better at solving tasks related to pathology, they still fell short of the strict requirements of an automated system for diagnosis. In 2014, Hamilton et al. were writing that "even the most state of the art AI systems failed to significantly change practice in pathology" [<a id='cpath-ref8-a' href='#cpath-ref8'>8</a>]. The problem is that medical diagnosis is generally made by integrating information from multiple sources: images from different modalities, expression of the symptoms by the patient, records of their medical history... AI systems can be very successful at relatively simpler sub-tasks (finding nuclei, delineating glands, grading morphological patterns in a region...), but they are just unable, at this point, to get the "big picture". Not to mention, of course, all the thorny obstacles to widespread adoption: trust in the system, regulatory issues, insurance and liability issues, etc., etc.</p>

<p>More than 65 years after the Cytoanalyzer, routine use of AI in clinical practice for pathology appears to be very close... but we're still not there yet. The performance of deep learning algorithms, combined with the widespread use of whole-slide scanners producing high-resolution digital slides, make the field of computer-assisted histopathology a very active and optimistic one at the moment. Still, even with the excellent results of Google Health's breast cancer screening system in clinical studies [<a id='cpath-ref9-a' href='#cpath-ref9'>9</a>], it's not clear that automated systems are ready for real practice.</p>

<p>The difficulties of our algorithms are in large part the same that were identified by Spriggs in 1969: it is difficult or impossible to get an "objective" assessment of pathological slides and, even with modern grading systems, inter-expert disagreement is high. This makes training and evaluating algorithms more difficult, and when dealing with a subject as sensitive as healthcare, any result short of near perfection will have a hard time getting adopted by the medical community... and by the patients.</p>

<h3>References</h3>
<ol>
    <li id='cpath-ref1'>[<a href='#cpath-ref1-a'>&#8593;</a>] A.I. Spriggs, "Automatic scanning for cervical smears", J. clin. Path. 22, suppl. (Coll. Path.), 3, 1-6 (1969). <a href="https://doi.org/10.1136/jcp.s2-3.1.1">doi:10.1136/jcp.s2-3.1.1</a></li>
    <li id='cpath-ref2'>[<a href='#cpath-ref2-a'>&#8593;</a>] J.H. Saltz, "Digital pathology - The big picture", Human Pathology 31(7), pp779-780 (2000). <a href="https://doi.org/10.1053/hupa.2000.9748">doi:10.1053/hupa.2000.9748</a></li>
    <li id='cpath-ref3'>[<a href='#cpath-ref3-a'>&#8593;</a>] Barbareschi, M., Demichelis, F., Forti, S. & Dalla Palma, P. Digital pathology: Science fiction? Int. J. Surg. Pathol. 8, 261–263 (2000). <a href="https://doi.org/10.1177/106689690000800401">doi:10.1177/106689690000800401</a></li>
    <li id='cpath-ref4'>[<a href='#cpath-ref4-a'>&#8593;</a>] May, M. A better lens on disease. Sci. Am. 302, 74–77 (2010). <a href="https://doi.org/10.1038/scientificamerican0510-74">doi:10.1038/scientificamerican0510-74</a></li>
    <li id='cpath-ref5'>[<a href='#cpath-ref5-a'>&#8593;</a>] Sucaet, Y. & Waelput, W. Digital Pathology. (Springer, 2014). <a href="https://doi.org/10.1159/isbn.978-3-318-05846-8">doi:10.1159/isbn.978-3-318-05846-8</a>.</li>
    <li id='cpath-ref6'>[<a href='#cpath-ref6-a'>&#8593;</a>] Tolles, W. E. SECTION OF BIOLOGY: THE CYTOANALYZER-AN EXAMPLE OF PHYSICS IN MEDICAL RESEARCH. Trans. N. Y. Acad. Sci. 17, 250–256 (1955). <a href="https://doi.org/10.1111/j.2164-0947.1955.tb01204.x">doi:10.1111/j.2164-0947.1955.tb01204.x</a>.</li>
    <li id='cpath-ref7'>[<a href='#cpath-ref7-a'>&#8593;</a>] Spencer, C. C. & Bostrom, R. C. Performance of the cytoanalyzer in recent clinical trials. J. Natl. Cancer Inst. 29, 267–276 (1962). <a href="https://doi.org/10.1093/jnci/29.2.267">doi:10.1093/jnci/29.2.267</a></li>
    <li id='cpath-ref8'>[<a href='#cpath-ref8-a'>&#8593;</a>] Hamilton, P. W. et al. Digital pathology and image analysis in tissue biomarker research. Methods 70, 59–73 (2014). <a href="https://doi.org/10.1016/j.ymeth.2014.06.015">doi:10.1016/j.ymeth.2014.06.015</a></li>
    <li id='cpath-ref9'>[<a href='#cpath-ref9-a'>&#8593;</a>] McKinney, S. M. et al. International evaluation of an AI system for breast cancer screening. Nature 577, 89–94 (2020). <a href="https://doi.org/10.1038/s41586-019-1799-6">doi:10.1038/s41586-019-1799-6</a></li>
</ol></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2021-03-17</pubDate>
    </item><item>
    <title>AI: better than your doctor?</title>
    <link>https://research.adfoucart.be/ai-better-doctor</link>
    <description><blockquote>AI system is better than human doctors at predicting breast cancer (<a href="https://www.newscientist.com/article/2228752-ai-system-is-better-than-human-doctors-at-predicting-breast-cancer/">J. Hamzelou, NewScientist</a>)
    AI Now Diagnoses Disease Better Than Your Doctor, Study Finds (<a href="https://towardsdatascience.com/ai-diagnoses-disease-better-than-your-doctor-study-finds-a5cc0ffbf32">D. Leibowitz, Towards Data Science</a>)
    AI Can Outperform Doctors. So Why Don’t Patients Trust It? (<a href="https://hbr.org/2019/10/ai-can-outperform-doctors-so-why-dont-patients-trust-it">C. Longoni and C. K. Morewedge, Harvard Business Review</a>)</blockquote>


  <img src='./img/pexels-alex-knight-2599244.jpg' class="img-rounded img-responsive" alt="" width='500px' />
  Image by Alex Knight from <a href="https://www.pexels.com/fr-fr/photo/photo-en-grand-angle-du-robot-2599244/">Pexels</a>.


<p>Machines have beaten us at chess and Go, they can drive our cars... and now they are better doctors? That last article from Harvard Business Review asks an interesting question. If machines can perform "with expert-level accuracy", why don't we trust them? In their study, they find that resistance to medical AI is driven by "a concern that AI providers are less able than human providers to account for consumers’ unique characteristics" [<a id='aibd-ref1-a' href='#aibd-ref1'>1</a>]. The problem, then, would be mostly one of perception. AI can be better than doctors in general, but we fear that they might not be better for us personally.</p>

<p>It's really hard to have a purely "rational" (whatever that means) discussion about the merits of AI versus human doctors, and part of the problem is in the terminology: "Artificial Intelligence". AI is a relatively vague categorization of a certain domain of computer science, but it is perhaps more importantly a term heavily associated with its historical use in science-fiction and scientific speculation. In fiction, AI is often used as a device to explore what it means to be human. It almost always comes along with an "artificial conscience". An AI is a person - or at least it tries to be. It's a notion that we find both in stories where AI is the "good guy" who just wants to be accepted into society, and in dystopian stories where an AI becomes "self-aware" and starts a war on humans.</p>

<p>So when we say "AI is better than human at...", we think about those AI, the self-aware machines. But modern AI, and in particular the kind of algorithms which are behind the "better than humans" headlines, has absolutely nothing to do with any of that. Deep Learning algorithms are tools which are entirely in the hands of the engineers and doctors that use them. They have no more "personality", "wants", or "conscience" than any other tool.</p>

<p>AI is "better than humans" at predicting breast cancer in the same sense as a bread slicer machine is "better than humans" at slicing bread. In both cases, humans are in control of what goes in, what comes out, and how the machine is set up.</p>

<p>It doesn't really make sense to talk about "better than human" performances for machine learning systems, because everything that work or doesn't work in such algorithms can be traced to humans. The engineers, computer scientists or mathematicians create a mathematical model which fully determines the range of data that the system can manage, and the kind of output that it can produce. Doctors and medical experts provide the data and annotations that will determine the parameters of the model, which only learns how to best reproduce what it's been shown.</p>

<p>If there is a bias in the results of an algorithm, it's not because "the AI" is biased, it's because the persons who designed the dataset and the learning process were. The term "AI" was reportedly coined in 1956 by John McCarthy, at the foundational "<a href="https://en.wikipedia.org/wiki/Dartmouth_workshop">Dartmouth workshop</a>". At the time, computer science was in its infancy, and the dream was that every aspect of human intelligence could eventually be replicated by a computer. That idea is now mostly reserved for the field of "Artificial General Intelligence" (AGI), and it's not certain that such a thing is even possible. AI, as it's mostly used today, is not "intelligence". It's a toolbox, a set of techniques that we can use to perform various tasks, but it doesn't have an "identity". It starts and stops at the press of a button, and does nothing more and nothing less than what it's programmed to do.</p>

<h3>What's the place of AI in medicine?</h3>

<p>AI will never be "better than doctors" because it's a meaningless proposition, but that doesn't mean AI has no place in medicine. AI techniques provide very useful tools which can vastly improve patient care.</p>

<p>The best way (in my opinion) to describe existing AI is as a very well indexed library of knowledge. The very complex models that compose modern Deep Learning methods can "store" in their parameters huge amount of medical knowledge, in the forms of links between observations and desired output. Trained algorithms can afterwards process huge amounts of data in short amounts of time.</p>

<p>In clinical practice, this may be very useful to "flag" potentially difficult cases (for instance, if a doctor's diagnosis is different from what the algorithm says, it may be useful to review the results), or to provide a quick first diagnosis in cases where there are no doctors available. It can also serve as a form of quality control. Perhaps more importantly, it can help a lot with ongoing research. Taking large amounts of patient information along with the evolution of their diseases in retrospective studies, machine learning algorithms can detect patterns associated with clinical outcomes. Sometimes, those patterns aren't associated with things that we already know about the disease, so this may give new avenues to explore. Avenues which may lead to dead ends, or to important discoveries. To find out which, human experts will remain well into the loop, at least for the foreseeable future.</p>

<h3>References</h3>
<ol>
    <li id='aibd-ref1'>[<a href='#aibd-ref1-a'>&#8593;</a>] C. Longoni, A. Bonezzi, C. Morewedge, " Resistance to Medical Artificial Intelligence ", Journal of Consumer Research 46:4 (2019). <a href="https://doi.org/10.1093/jcr/ucz013">doi:10.1093/jcr/ucz013</a></li>
</ol></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2021-02-19</pubDate>
    </item><item>
    <title>Artefact detection in Digital Pathology</title>
    <link>https://research.adfoucart.be/artefact-detection-in-digital-pathology</link>
    <description><p>A digital pathology task that illustrates quite well the "imperfect nature of digital pathology annotations" that I mentioned in the <a href="/fifty-shades-unet">previous post</a> is artefact detection. In the first post of this blog, I presented the main steps of <a href="/what-is-digital-pathology">Digital Pathology</a>, from the extraction of the tissue sample to the digitalization of the slide. Throughout this process, the tissue is manipulated, cut, moved, stained, cleaned, sometimes frozen, and more generally exposed to the elements. This can cause serious damage, as illustrated in the figure below. Sometimes, problems can also occur during the acquisition process, leading to blurriness or contrast issues.</p>


  <img src='./img/artefact_example.png' class="img-rounded img-responsive" alt="Artefact example" width='500px' />
  Example of an artefact caused by tissue manipulation.


<p>Depending on the severity of those issues, we may have to discard parts of the image from further analysis, redo the acquisition process or, in extreme cases, require a new slide to be produced. This can have a big negative impact on the pathology workflow, and it is also a potential source of uncertainty on the results. Typically, this "quality control" is done manually. This can take a lot of time, and it is a very subjective process which can lead to many mistakes [<a id='addp-ref1-a' href='#addp-ref1'>1</a>]. Some algorithms had been proposed in the past to detect some specific types of artefact, such as blurry regions [<a id='addp-ref2-a' href='#addp-ref2'>2</a>] or tissue-folds [<a id='addp-ref3-a' href='#addp-ref3'>3</a>], but our goal was to develop a general-purpose method to catch most artefacts in one pass.</p>

<h3>The dataset</h3>

<p>Whose slide images are very large. Artefacts can be of varying sizes and shapes, making them extremely tedious to annotate. This is a situation that will always lead to imperfect annotations. We had one slide annotated in details by an histology technologist, and even in that slide many of the annotations were missing, or imprecise. To be able to get enough data to train a neural network, I quickly annotated the rest of the dataset based on the technologist's example, and I'm not an expert at all. As a result, we had a dataset with annotations that were rough, didn't really match the contours of the artefacts, and where a lot of artefacts were not marked at all. We had 26 slides in total. Slides are very large images, so while twenty-six seems like a very low number for training a neural network, we can actually extract a very large number of smaller patches from those slides to satisfy our needs.</p>


  <img src='./img/wsi_artefact.png' class="img-rounded img-responsive" alt="Whole-slide images" width='1000px' />
  Whole-slide images in the dataset, shown here in very low resolution.


<p>An interesting thing about the data is that it has different types of staining, as shown in the figure above. Some of the slides have been stained with a standard "Haematoxilyn & Eosin" treatment, which is typical to show the general morphology of the tissue, while others have been stained with immunohistochemistry biomarkers, highlighting tumoral tissue in brown.</p>

<p>One of the first question we always have to answer once we have a dataset is: what do we use to train our models, and what do we use to evaluate them?</p>

<p>In this case, as we had one slide with "better" annotations than the rest, we decided to keep it for testing. There were two good reasons for that: first, this would make the evaluation more likely to be meaningful; second, that slide came from another source and another staining protocol as the others, which would help us test the "generalization" capability of our methods.</p>

<h3>What makes the difference?</h3>

<p>The main thing we wanted to see was: what parts of the <a href="/machine-learning-pipeline">machine learning pipeline</a> were really important, and had an impact on the results? What we initially did was to take a relatively small, straightforward neural network architecture, and to test many different small changes in its depth ("how many layers") and width ("how many neurons per layer"). We also looked at whether we should use different levels of magnification on the image, or if we should try to "balance" the dataset by sampling more artefacts, as they were more examples of normal tissue than of artefact tissue in the data. Finally, we tried to compare using a "segmentation" output (predicting for each pixel if there is an artefact or not) or a "detection" output (predicting for the whole image patch if there is an artefact or not).</p>

<p>We published our first results at the "CloudTech 2018" conference [<a id='addp-ref4-a' href='#addp-ref4'>4</a>]. What we found most interesting was that, quite clearly, the details of the network architecture didn't really matter. The balance of the dataset and how we decided to produce our "per pixel" output (directly from the network, or by combining the detection results from overlapping patches) were the main factors that influenced the final results.</p>

<p>Let's have a look at this result, for instance:</p>


  <img src='./img/artefact-balance.gif' class="img-rounded img-responsive" alt="Effect of dataset balancing" width='500px' />
  Effects of dataset balancing: results of the same network trained with random sampling, 25% artefacts, 50% artefacts and 75% artefacts. Blue = annotated artefact detected by the network, Red = detected by the network but not in the annotations, Black = annotated artefact not detected by the network.


<p>Here, we have the same network, which uses patch-based detection, and has been trained using different data sampling. If we randomly sample the patches from the original dataset, we don't have enough examples of artefacts, and the network misses most of them. If we increase the percentage of artefact examples in the training batches, the network becomes a lot more sensitive. Obviously, this comes at the risk of falsely detecting normal tissue.</p>

<h3>How can we evaluate those results?</h3>  

<p>Evaluation metrics have to be related to the usage that we want to make of the results. In this case, the goals can be to assess the "quality" of the slide (or: to know how much of the slide is corrupted), and to remove damaged areas from further processing. For the latter task, we can reformulate it as: finding the "good" areas that can be used later.</p>

<p>There is no single metric that can really give us all that information. We computed the accuracy (which proportion of the pixels were correctly classified), the "True Positive Rate" (which proportion of the annotated artefacts did we correctly identify), the "True Negative Rate" (which proportion of the normal tissue did we correctly identify), and the "Negative Predictive Value" (which proportion of the tissue we identified as normal is actually normal). A low TPR would mean that we missed many artefacts; a low TNR that we removed a large portion of normal tissue; a low NPV that our "normal tissue" prediction is unreliable. To take the subjective nature of the evaluation into account, we also added a simple qualitative assessment, which we formulated as a binary choice: could this result be reasonably used in a digital pathology pipeline?</p>

<p>None of those metrics were really satisfactory. The trend that "what goes around the network" had a lot more impact than minute changes within, however, was clear enough that we could draw useful conclusions from those first experiments.</p>

<h3>The main lesson</h3>

<p>These first experiments with artefact detection showed that the way we prepared our dataset, and the way we defined our problem, had more impact on our results than the network itself. This is particularly visible when working on a dataset with very imperfect annotations, where just fitting the raw data into a deep learning model will lead to extremely poor results.</p>

<p>A lot of published research in computer vision for digital pathology is done on datasets published in challenges. These datasets tend to be very "clean", with precise annotations and pre-selected regions of interest. This makes it easier to train machine learning models, and it also makes it a lot easier to test and compare algorithms with one another. But the question we wanted to explore now was: as real-world datasets from research or clinical practice are not generally as clean as challenge datasets, shouldn't we be cautious about the conclusions we draw from these challenge results?</p>

<h3>References</h3>

<ul type='1'>
    <li id='addp-ref1'>[<a href='#addp-ref1-a'>&#8593;</a>] A. Janowczyk et al. (2019) "HistoQC: An Open-Source Quality Control Tool for Digital Pathology Slides", JCO Clinical Cancer Analytics, DOI: <a href="https://doi.org/10.1200/CCI.18.00157">10.1200/CCI.18.00157</a></li>
    <li id='addp-ref2'>[<a href='#addp-ref2-a'>&#8593;</a>] X. Moles Lopez et al. (2012) "An Automated Blur Detection Method for Histological Whole Slide Imaging", PLoS One 8(12), DOI: <a href="https://doi.org/10.1371/journal.pone.0082710">10.1371/journal.pone.0082710</a></li>
    <li id='addp-ref3'>[<a href='#addp-ref3-a'>&#8593;</a>] A. Kothari et al. (2013) "Eliminating tissue-fold artifacts in histopathological whole-slide images for improved image-based prediction of cancer grade", Journal of Pathology Informatics 4:22, DOI: <a href="https://doi.org/10.4103/2153-3539.117448">10.4103/2153-3539.117448</a></li>
    <li id='addp-ref4'>[<a href='#addp-ref4-a'>&#8593;</a>] A. Foucart et al. (2018) "Artifact Identification in Digital Pathology from Weak and Noisy Supervision with Deep Residual Networks", Proc. CloudTech 2018, DOI: <a href="https://doi.org/10.1109/CloudTech.2018.8713350">10.1109/CloudTech.2018.8713350</a></li>
</ul></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2021-01-20</pubDate>
    </item><item>
    <title>Fifty shades of U-Net</title>
    <link>https://research.adfoucart.be/fifty-shades-unet</link>
    <description><p>Poster sessions in conferences are an exercise that I generally dislike. If you've never seen one, the principle is fairly simple: you take the largest halls and corridors at your disposal in the conference center, give each researcher a one meter wide space to hang a poster, and for one hour they stay next to their poster waiting for someone to come and ask questions about it instead of going for another coffee or biscuit (or sandwich, in the particularly bad cases where the poster session happens during lunch break).</p>


  <img src='./img/Printed_poster_session.jpg' class="img-rounded img-responsive" alt="Poster session" width='200px' /></a>
  A particularly awful looking poster session. (CC BY-SA 4.0, <a href="https://en.wikipedia.org/w/index.php?curid=60827122">Wikipedia</a>)


<p>While I think it's generally ineficient as a way of presenting your research, it does provide an unique opportunity of getting a quick overview of "what's going on" in the field. By quickly browsing the posters, which are generally arranged somewhat thematically, you get a sense of the main trends of the domain at the time.</p>

<p>In April 2019, when I attended the <a href="https://biomedicalimaging.org/2019/">ISBI</a> conference, it felt like everywhere we looked we could see the same picture looking back at us:</p>


  <img src='./img/unet.png' class="img-rounded img-responsive" alt="UNet architecture" width='500px' /></a>
  U-Net architecture, from Ronneberger et al [<a id='fsu-ref1-a' href='#fsu-ref1'>1</a>].


<p>U-Net. It was introduced in 2015 at another conference (MICCAI) and quickly became a very popular choice for medical image segmentation. The two reasons for that were that it got good results on several different challenges and datasets, and that the authors <a href="https://lmb.informatik.uni-freiburg.de/people/ronneber/u-net/">released the trained network and the source code of the model</a>, so anyone who wanted to try it out didn't have to re-implement everything.</p>

<p>By 2019, U-Net was everywhere. It quickly became the default option for medical image segmentation, along with several extensions and adaptations. From the ISBI poster session, it seemed like any segmentation problem could now be approached in the same way: get a bunch of annotations, fit a U-Net on your dataset, publish the results.</p>

<p>The problem of course is: what are we learning from all that? What's the point? Have we just solved segmentation and that's it?</p>

<p>One interaction I had during this poster session stuck with me. I was looking at a table of results on X-Ray images, which used the <a href="https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient">Dice (or F1)</a> score as a metric. The results were very close to each other, and when I wondered if they thought it was a significant difference, they replied that even though the scores were very similar, they could see a clear difference visually on the shape of the borders of the object, which is what they were mostly interested in. Why, then, didn't they use another, <a href="https://en.wikipedia.org/wiki/Hausdorff_distance">more relevant metric</a>? They apparently hadn't thought about it.</p>

<p>This stuck with me because it's not an isolated case. The F1/Dice score is by far the most widely used metric in segmentation problems, even for problem where we know it's not really good. Why? Because it's familiar, and if we all use the same metrics all the time it's easier to compare things together. But that means that we are now pursuing tiny improvements on metrics which aren't necessarily related to the clinical task that we are actually trying to solve.</p>

<p>An important feature of digital pathology tasks is that they are often difficult to quantify. Pathologists will typically integrate different observations from different images, along with clinical information about the patient, in order to produce their diagnosis. These assessment, however, are often subjective. That's part of the reason we want more computational methods to help with these tasks: to provide more objectivity. But we are now facing a very difficult problem to measure how "good" an algorithm is.</p>

<p>Let's take a quick example and go back to the <a hred="/mitos12-learning-bad-challenge-design">mitosis detection challenge</a> we talked about earlier. Mitosis detection is not a "pathology" task by itself. It's used, for instance, as part of the <a href="https://en.wikipedia.org/wiki/Breast_cancer_classification">Nottingham system</a> for breast cancer classification, alongside other factors. When assessing how good an algorithm is, the question we should be answering is: does it make errors which would lead to a bad diagnostic? And as soon as we get into that, we make the problem a lot more complex, because not only is that extremely hard to measure (as we can only do that on retrospective studies where we know the clinical outcome), but it only leads to more questions, such as: should we penalize more an underestimation of the cancer severity, or an overestimation? Do we prefer an algorithm that's closer to the reality "on average", or an algorithm which has very few outliers?</p>

<p>With all these criteria, the question of "which algorithm is the best" becomes impossible to answer. So we simplify the problem. Instead of asking "which algorithm leads to a better diagnosis", we ask "which algorithm detects the same mitosis as the expert who annotated the images in the dataset", which may lead to a radically different answer.</p>

<p>This is what I want to focus on in my thesis (and here in the next posts): how do we correctly take into account the imperfect nature of digital pathology annotations when we train and test deep learning algorithms? How do we take into account mistakes, imprecisions, and inter-expert disagreement? In short: how do we ensure that we don't use up all our energy chasing for results which don't actually teach us anything either about pathology, or about Deep Learning?</p>

<h3>References</h3>
<ol>
    <li id='fsu-ref1'>[<a href='#fsu-ref1-a'>&#8593;</a>] Ronneberger et al., "U-Net: Convolutional Networks for Biomedical Image Segmentation", Proc. MICCAI 2015. <a href="https://doi.org/10.1007/978-3-319-24574-4_28">doi:10.1007/978-3-319-24574-4_28</a></li>
</ol>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2021-01-11</pubDate>
    </item><item>
    <title>How to train your neural network</title>
    <link>https://research.adfoucart.be/how-to-train-your-network</link>
    <description>

<p>With all the tools at our disposal now in libraries such as Google's <a href="https://www.tensorflow.org/">Tensorflow</a> or Facebook's <a href="https://pytorch.org/">PyTorch</a>, deep neural networks have become increasingly easy to develop, train and use. While this is in general a very good thing, it also makes it very common to treat deep learning algorithms as "black boxes", mysterious beasts which magically transform a "training set" into a trained model with excellent performances on the accompanying "test set". Deep neural networks, however, are only black boxes if we don't open them up and take a peak.</p>

<p>As I've written earlier in the post "<a href="./every-ml-algorithm">Every machine learning algorithm</a>", a "machine learning model" is essentially a function mapping an input (the "data") to an output (the "prediction"). More complex models are characterized by a larger number of parameters in this function. What differentiates a deep neural network from a simple linear model is the much higher complexity of the model which, instead of just being able to find a "straight line" in the space of the input points, is capable of finding essentially any shape. The "shape" in question will be determined by the parameters of the network, which as we've seen in the "<a href="./dl-building-blocks">building blocks of Deep Learning</a>" are the weights of the connections between the "neurons".</p>

<p>The big question is, of course: how do we find the value of those parameters?</p>

<h3>The cost of an error</h3>

<p>The first thing that we need to build a training method is a way to determine if an output is correct or not.</p>

<p>Let's go back to our "face detection" problem. The input of our model are the pixel values of a small image patch. The output value is a number representing the probability of the image containing a face.</p>


  <img src='./img/faces_regions.png' class="img-rounded img-responsive" alt="Image patches" width='600px' /></a>
  Fig. 1 - Image patches given as input to a model <a href="https://www.pexels.com/photo/family-of-four-walking-at-the-street-2253879/">Original photo</a> by Emma Bauso from Pexels.


<p>The "correct output" for the image patches show in Fig. 1 would be: [1, 1, 1, 0, 0, 0] (from left to right and top to bottom). Let's imagine that, for a given set of parameters, our model outputs the values: [0.71, 0.23, 0.55, 0.12, 0.44, 0.05]. What is the "error" made by the model? One thing we could do is just look at the difference between each pair of "expected/predicted" values: 1-0.71, 1-0.23, etc... So, we would get:</p>

<table>
	<tr>
		<td>Correct output</td>
		<td>1</td>
		<td>1</td>
		<td>1</td>
		<td>0</td>
		<td>0</td>
		<td>0</td>
	</tr>
	<tr>
		<td>Model prediction</td>
		<td>.71</td>
		<td>.23</td>
		<td>.55</td>
		<td>.12</td>
		<td>.44</td>
		<td>.05</td>
	</tr>
	<tr>
		<td>Error</td>
		<td>.29</td>
		<td>.77</td>
		<td>.45</td>
		<td>-.12</td>
		<td>-.44</td>
		<td>-.05</td>
	</tr>
</table>

<p>The mean error would be the average value of this vector (here: 0.15), but it's not a very useful measure. Imagine for instance that we had a completely wrong prediction vector: [0, 0, 0, 1, 1, 1]. The error vector would be [1, 1, 1, -1, -1, -1], for an average of 0. "Positive" errors and "negative" errors are balancing each other, which is not exactly what we are looking for.</p> 

<p>The mean absolute error would remove that problem by first taking the absolute value of the error. In our first error vector, this would lead to a cost of 0.35, and in our "completely wrong" example to a cost of 1, which seems a lot more reasonable.</p>

<p>The more commonly used cost functions in machine learning are the <a href="https://en.wikipedia.org/wiki/Mean_squared_error">mean squared error</a> (for regression problems), and the <a href="https://en.wikipedia.org/wiki/Cross_entropy">cross-entropy</a> (for classification problems). Both have the particularity of giving a much higher cost for larger errors (in other words: they consider it's better to be a little wrong most of the time than to be very wrong on rare occasions).</p>

<p>Let's now imagine that we have all the time and computer power we want. We could take every single combination of parameters in our model, and for each of those compute this cost on our training dataset. Whichever combination gives us the lowest "cost" should be the best. If we had only one parameter in our model, we could make a plot showing the cost for every value of the parameter. This would typically look something like this:</p>


  <img src='./img/cost-param.png' class="img-rounded img-responsive" alt="Cost function" width='400px' /></a>
  Fig. 2 - Cost depending on the value of the parameter.


<p>Great. But as we don't have all the time and computer power we want, what do we do?</p>

<h3>Going downhill</h3>

<p>The idea for solving this kind of problem (finding the minimum of a very complex function) comes from all the way to 1847, and a communication to the French Academy of Sciences by Augustin Cauchy [<a id='httyn-ref1-a' href='#httyn-ref1'>1</a>].</p>

<p>Cauchy's idea is very simple. We start from an arbitrary "position" in this function (meaning in our cases: arbitrary values for all parameters of the model). We can look at the gradient of the function relative to those parameters. In other words: we find the "direction" where moving the parameters makes the function go down.</p>

<p>Now that we have a direction, we move the parameters by a small step, and we start again. Step by step, we go down the cost function, until we hit a point where there is nowhere to go except back up. Then, we know that we've hit a minimum, as illustrated below.</p>


  <img src='./img/gradient-descent.png' class="img-rounded img-responsive" alt="Gradient Descent" width='400px' /></a>
  Fig. 3 - An illustration of the Gradient Descent process


<p>This is the basic process of Gradient Descent. As all parts of a machine learning pipeline, it involves several choices, which will be part of the hyper-parameters. The main choices here are:</p>

<ul>
	<li>How do we choose the "initial parameters"?</li>
	<li>By how much do we "move" at each step?</li>
	<li>How do we know that we are at the global, not a local, minimum?</li>
</ul>

<p>The answer to those will depend on the model and the dataset, but in general the initialization will be coming from some form of random generator, the movement will be determined by a learning rate which may vary as the learning progresses, and for the third question, we don't.</p>

<h3>How far is too far?</h3>

<p>I wrote above that we could in theory find the "best parameters" by computing the cost for every possible value they can take on our training dataset. That was not totally correct. At the very least, it depends on what we define as "best". The minimum of the cost function computed on the training dataset that will give us a prediction as close as possible to the "target" that we gave to the model... But as we've seen in "<a href="./every-ml-algorithm">Every machine learning algorithm</a>", that's not always a good thing.</p>

<p>If our model is complex enough, we can end up basically storing the entire training dataset in its parameters. This leads to a situation where it is perfect at predicting the output for things that is has already seen but has a completely unpredictable behaviour everywhere else. That's overfitting, and as Deep Neural Networks often have millions of parameters, they can easily fall into that trap.</p>

<p>There are things that can be done in the model itself, or in the cost function, to encourage generalization. This is a whole topic by itself which I won't get into: regularization.</p>

<p>But at a most fundamental level, what we really need to do is to evaluate the cost on data that isn't used directly for the gradient descent. When we have a dataset that we want to use for training a machine learning model, we split it in three distinct parts: the training set is used by the model to find the best possible parameters; the validation set is used to evaluate, during the training, how well the model performs on unseen data; and finally the test set is kept aside until the very end, when we have finalized our model, to get a representative measure of how well it actually performs.</p>

<p>Overfitting, within that scheme, is very easy to detect. It happens when the cost keeps getting lower when computed on the training set but starts going up when computed on the validation set. This is a clear sign that we have to stop the training, or risk losing generalization capabilities.</p>

<h3>Gradient descent for neural networks</h3>

<p>There is one last thing I want to mention before we close this series of articles on the main principles of Deep Learning.</p>

<p>To compute the "gradient" that we mentioned before, we need to be able to formulate the cost function as a function of the parameters of the model, and we need to be able to find its derivative. For a neural network with multiple layers, this is not possible. The relationship between the output and the weights applied in the first layer is far too complex. So what can we do?</p>

<p>In the 1970s and 1980s, the idea of back-propagation for neural networks was developed. Paul Werbos published a version of it in 1982 [<a id='httyn-ref2-a' href='#httyn-ref2'>2</a>], and the one most commonly cited as popularizing the idea was published in 1986 by David Rumelhard, Geoffrey Hinton and Ronald Williams [<a id='httyn-ref3-a' href='#httyn-ref3'>3</a>].</p>


  <img src='./img/werbos.png' class="img-rounded img-responsive" alt="Werbos illustration" width='400px' /></a>
  Fig. 4 - Illustration from Werbos, 1982 [<a id='httyn-ref2-a' href='#httyn-ref2'>2</a>].


<p>A good detailed step-by-step explanation is provided by Michael Nielsen on <a href="http://neuralnetworksanddeeplearning.com/chap2.html">his website</a>, but the main idea is this:</p>

<p>While it's hard to formulate the gradient with respect to all the weights in the network, it's easy to formulate the gradient with respect to the weights of the last layer. In the back-propagation algorithm, we start by computing those gradients, then we "propagate" the gradients "backward" (hence the title) to the previous layer.</p>

<p>In a sense, it uses the same idea as neural networks in general: instead of solving one very hard problem, we solve millions of comparatively easy problems sequentially, and let the connections between those problems add complexity.</p>

<h3>Conclusion</h3>

<p>These are the core components of all Deep learning algorithms:</p>

<ul>
	<li>A robust "pipeline" that transforms the input data into something that the network can handle and interprets its outputs in a way that can be used.</li>
	<li>A network architecture which creates enough complexity, through multiple layers, convolutions, pooling and other operations, to accurately represent the mapping between the input and the output.</li>
	<li>An optimization method, generally based around gradient descent and backpropagation, to set the best weights for the connections in this network.</li>
	<li>A large, good dataset that can provide a reliable source of training, and a good evaluation of the results through the test set.</li> 
</ul>

<h3>References</h3>
<ol>
    <li id='httyn-ref1'>[<a href='#httyn-ref1-a'>&#8593;</a>] A. Cauchy, "Méthode générale pour la résolution des systèmes d'équations simultanées", in Compte-Rendu des séances de l'académie des sciences, 5 juillet 1847, p536-538 <a href="https://gallica.bnf.fr/ark:/12148/bpt6k2982c/f540.item">Available online</a></li>
    <li id='httyn-ref2'>[<a href='#httyn-ref2-a'>&#8593;</a>] P. Werbos, "Applications of Advances in Nonlinear Sensitivity Analysis" <a href="http://werbos.com/Neural/SensitivityIFIPSeptember1981.pdf">Available online</a></li>
    <li id='httyn-ref2'>[<a href='#httyn-ref3-a'>&#8593;</a>] D. Rumemhart et al, "Learning representations by back-propagating errors" <a href="https://www.nature.com/articles/323533a0.pdf">Available online</a></li>
</ol>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-12-14</pubDate>
    </item><item>
    <title>The building blocks of Deep Learning</title>
    <link>https://research.adfoucart.be/dl-building-blocks</link>
    <description>

<p>If you read a paper describing a deep learning solution to any sort of problem, you'll probably end up looking at something like this:</p>


  <img src='./img/shortres.png' class="img-rounded img-responsive" alt="ShortRes architecture" width='500px' /></a>
  Fig. 1 - Schematic representation of the ShortRes architecture, from [<a id='dlbb-ref1-a' href='#dlbb-ref1'>1</a>].


<p>Or this:</p>


  <img src='./img/unet.png' class="img-rounded img-responsive" alt="UNet architecture" width='500px' /></a>
  Fig. 2 - Schematic representation of the Unet architecture, from [<a id='dlbb-ref2-a' href='#dlbb-ref2'>2</a>].


<p>Schematic representations of deep neural network allow us to quickly represent very complex structures. The reason we can use those representations and, generally, convey the general idea of what the network does, is that most architectures are made of the same basic building blocks. This allows us to reduce relatively complex operations into a simple "block" or "layer" whose purpose will be well understood by other researchers.</p>

<p>I'll take a look at those blocks in a moment, but first I want to take a magnifying glass into the network to observe the element at the base of all artificial neural networks: the artificial neuron.</p>

<h3>The artificial neuron</h3>

<p>In 1943, McCulloch and Pitts introduced a mathematical model for the activity of a single neuron [<a id='dlbb-ref3-a' href='#dlbb-ref3'>3</a>]. This had a huge influence on the history of artificial intelligence: anything that can be written in a formal mathematical language can be replicated by a computer. If a computer can have individual components behaving like neurons... Why couldn't you have an artificial brain?</p>


  <img src='./img/mpneuron.png' class="img-rounded img-responsive" alt="The artificial neuron" width='400px' /></a>
  Fig. 3 - The artificial neuron.


<p>As you can imagine, since it's 2020 and we don't have an artificial brain yet, it turned out to be a bit more complex than that. But the general principle of McCulloch and Pitts' neuron are still valid for modern neural network: input signals (from the "dendrites"), which can have different weights (the "strength" of the connection), positive ("excitatory") or negative ("inhibitory") are summed into a core unit which decides if the signal should be passed on to the output (through the "axon" and into the "synapses").</p>

<p>In an artificial neural network, in general, the structure of the connections (i.e. which neuron is connected to which) is fixed, but the weights of the connections are the parameters that the machine learning algorithm will try to optimize. The structure as well as the activation functions (the function that decides what the output should be based on the aggregated inputs) are hyper-parameters, set by the designer of the solution.</p>

<p>Individual neurons are very limited in what they can do. Connected in large networks, however, they are powerful mathematical tools. Let's start by looking at dense layers.</p>

<h3>Dense layers</h3> 

<p>Dense layers are very easy to explain. All the neurons in the layer are connected to all the outputs from the previous layer, as illustrated below.</p>


  <img src='./img/verysmall_mlp.png' class="img-rounded img-responsive" alt="The artificial neuron" width='400px' /></a>
  Fig. 4 - A very small network.


<p>To determine the output of such a network, we can just follow through the connections. If we have two inputs \(x_1\) and \(x_2\), we will have \(h_1 = A_h(w_{11}x_1 + w_{21}x_2)\) as the output of neuron \(h_1\). We can do the same for \(h_2\), then move on to the output: \(y = A_y(v_1 h_1 + v_2 h_2)\). \(A_y\) and \(A_h\) being the activation functions of the different neurons.</p>

<p>One of the key features of neural networks is that the individual operations that are performed in the network are very simple, but we can get very complex outputs (and we can in fact approximate "virtually any function of interest" [<a id='dlbb-ref4-a' href='#dlbb-ref4'>4</a>]) by just adding enough neurons to the network.</p>

<p>The problem with dense layers, however, is that the number of parameters grows very rapidly with the number of neurons, and often quickly become impractical. This is particularly true in image analysis. If our input is a 256x256 pixels image, that's already 65.536 input neurons (196.608 if the image is in colours). If we need, for instance, two layers of 1000 neurons each followed by a single output neurons, we will have around 66 million parameters in the model (more than 197 million for colour images). As the general "rule of thumb" in machine learning is that, to train a model, we need "a lot more" annotated examples than we have parameters, this makes things really hard.</p>

<h3>Convolutional layers</h3>

<p>Convolutional layers are arguably one of the key things that made artificial neural network a practical solution for image analysis rather than a fun curiosity. Convolutional networks first appeared in 1980 with Fukushima's "Neocognitron" [<a id='dlbb-ref5-a' href='#dlbb-ref5'>5</a>], but it really became a staple of Deep Learning in 1989 with Yann LeCun's "handwritten digit recognition" solution [<a id='dlbb-ref6-a' href='#dlbb-ref6'>6</a>].</p>

<p>The basic concept of convolutional layers is that, instead of connecting every neuron from a layer to every neuron of the next, the connections are limited to a "neighbourhood". If an image passes through a convolutional layer, the output will also be an image, filtered by a "convolutional kernel". The values and size of the kernel will determine the effect of the filter. For instance, in the figure below, we apply a 3x3 kernel designed to get a strong response from horizontal edges.</p>


  <img src='./img/face_conv_filter.png' class="img-rounded img-responsive" alt="Result of a convolutional filter" width='600px' /></a>
  Fig. 5 - Output of a 3x3 convolutional filter designed to detect horizontal edges. White = strong positive response (dark region above a light region), black = strong negative response (light region above a dark region), grey = weak response (no horizontal gradient). <a href="https://www.pexels.com/photo/family-of-four-walking-at-the-street-2253879/">Original photo</a> by Emma Bauso from Pexels.


<p>In a convolutional neural network, each layer will consist in a set of different filters applied to the previous layer.</p>

<p>There are two distinct advantages here, especially for image analysis. The first is that we keep, built into the structure of the network itself, a sense of the spatial organization of the information. With a dense layer, the network has no way of knowing if two pixels are neighbours. Here, the network can extract features starting from neighbouring pixels, and only combining them together at the scale of the image later in the network. The second is that we need less parameters to extract useful information. In a dense network, if we learn a useful local filter at the top-left of the image using 9 connections, we have to use 9 other connections to learn the same filter at the bottom right. In a convolutional network, we learn filters which are applied on the entire image.</p>

<p>For instance, if instead of two dense layers with 1000 neurons, we use two convolutional layers with 64 filters (which will give us about 4 million neurons per layer), we will only need about 40.000 parameters to get what will probably be much more interesting features.</p>

<h3>Pooling and upsampling</h3>

<p>A downside of convolutional architectures is that neurons lose the overall context of the image, as they can only see a small part of it. It's very difficult to recognize objects, or to perform other high-level computer vision tasks, when every component of the algorithm has a very limited field of view.</p>

<p>Pooling layers are used to downsample an image. This allows the next layer to have a wider field of view, at the cost of a lower resolution. The most common pooling technique is the "max-pooling", which selects the maximum value out of a small neighbourhood and feeds it forward in the network. Pooling effectively summarizes the information contained in a neighbourhood, and makes the network invariant to small translations [<a id='dlbb-ref7-a' href='#dlbb-ref7'>7</a>].</p>

<p>Upsampling layers have the opposite effect: they upscale the feature maps, either through a "transposed convolution" operation, or through interpolation. This is typical of segmentation networks, where we need the output to have the same size as the input. Many segmentation network will go through a "downsampling" phase, extracting features with a progressively higher semantic level as the network combines low-level features and puts them into their respective context, followed by an "upsampling" phase, where those features are extrapolated into a "prediction map" giving a class probability for every pixel of the image. This can be seen in both figures 1 and 2 above.</p> 

<h3>...and all the rest</h3>

<p>There are many other types of layers, with more specific purposes. For instance, Dropout units [<a id='dlbb-ref8-a' href='#dlbb-ref8'>8</a>] act as a sort of "filter", randomly and temporarily removing neurons of a layer during training. This means that we actually train slightly different, smaller versions of the network in parallel. This has a similar result to training multiple classifiers and averaging their results, which is to make it more likely to generalize well.</p>

<p>Batch normalization is another modifier than we can add to a layer. It standardizes its inputs by estimating their means and variances throughout training. This makes the learning process more stable, and tends to accelerate convergence [<a id='dlbb-ref9-a' href='#dlbb-ref9'>9</a>].</p>

<p>Skip-connections are another very common feature of modern deep networks. The idea of a skip-connection is to add shortcuts through the network, where the output from an "earlier" layer is reintroduced later in the network. This can be done over a very short distance, as in residual units [<a id='dlbb-ref10-a' href='#dlbb-ref10'>10</a>], which skip a few convolutions, or over large parts of the network as can be found for instance in the U-Net architecture [<a id='dlbb-ref2-a' href='#dlbb-ref2'>2</a>].</p>

<h3>Conclusion</h3>

<p>Network architectures of all sizes and shapes exist, and the state of the art has moved from the relatively short networks of the "early days" of the 00s, with half a dozen layers, to increasingly deep and complex networks with more than a 100 layers in the mid-10s, somewhat moving back to smaller, more efficient architectures in more recent years, as the focus shifted a bit from getting the absolute best performance to getting solutions which can work on regular computers and smartphones for the general public.</p>

<p>There is no doubt that the specific design of the network will influence how well it can model a specific dataset, how well it can perform a specific task, and how efficiently it will be able to learn.</p>

<p>But modern, "general-purpose" networks will work well for a variety of tasks, and more often than not, the most important part of the design process is to think about how to train it. How does a neural network learn? That will be our next topic.</p>

<h3>References</h3>
<ol>
    <li id='dlbb-ref1'>[<a href='#dlbb-ref1-a'>&#8593;</a>] Foucart et al., "SNOW: Semi-Supervized, Noisy and/or Weak data for Deep Learning in Digital Pathology", Proc. ISBI 2019. <a href="http://dx.doi.org/10.1109/ISBI.2019.8759545">doi:10.1109/ISBI.2019.8759545</a></li>
    <li id='dlbb-ref2'>[<a href='#dlbb-ref2-a'>&#8593;</a>] Ronneberger et al., "U-Net: Convolutional Networks for Biomedical Image Segmentation", Proc. MICCAI 2015. <a href="https://doi.org/10.1007/978-3-319-24574-4_28">doi:10.1007/978-3-319-24574-4_28</a></li>
    <li id='dlbb-ref3'>[<a href='#dlbb-ref3-a'>&#8593;</a>] W. McCulloch and W. Pitts, "A logical calculus of the ideas immanent in nervous activity", Bulletin of Mathematical Biophysics, 1943. <a href="https://doi.org/10.1007/BF02478259">doi:10.1007/BF02478259</a></li>
    <li id='dlbb-ref4'>[<a href='#dlbb-ref4-a'>&#8593;</a>] K. Hornik et al, "Multilayer Feedforward Networks are Universal Approximators", Neural Networks, 1989. <a href="https://doi.org/10.1016/0893-6080(89)90020-8">doi:10.1016/0893-6080(89)90020-8</a></li>
    <li id='dlbb-ref5'>[<a href='#dlbb-ref5-a'>&#8593;</a>] K. Fukushima, "Neocognitron: A self-organizing neural network model for a mechanism of pattern recognition unaffected by shift in position", Biological Cybernetics, 1980. <a href="https://doi.org/10.1007/BF00344251">doi:10.1007/BF00344251</a></li>
    <li id='dlbb-ref6'>[<a href='#dlbb-ref6-a'>&#8593;</a>] LeCun et al, " Back-propagation applied to handwritten zip code recognition", Neural Computation, 1989. <a href="https://doi.org/10.1162/neco.1989.1.4.541">doi:10.1162/neco.1989.1.4.541</a></li>
    <li id='dlbb-ref7'>[<a href='#dlbb-ref7-a'>&#8593;</a>] I. Goodfellow, Y. Bengio, A. Courville (2016) "Deep Learning", MIT Press <a href="https://www.deeplearningbook.org/">https://www.deeplearningbook.org/</a></li>
    <li id='dlbb-ref8'>[<a href='#dlbb-ref8-a'>&#8593;</a>] N. Srivastava et al, "Dropout: A Simple Way to Prevent Neural Networks Overfitting", Journal of Machine Learning Research <a href="https://dl.acm.org/doi/10.5555/2627435.2670313">doi:10.5555/2627435.2670313</a></li>
    <li id='dlbb-ref9'>[<a href='#dlbb-ref9-a'>&#8593;</a>] S. Ioffe and C. Szegedy, "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift", Proc. ICML, 2015. <a href="https://dl.acm.org/doi/10.5555/3045118.3045167">doi:10.5555/3045118.3045167</a></li>
    <li id='dlbb-ref10'>[<a href='#dlbb-ref10-a'>&#8593;</a>] K. He et al, "Deep Residual Learning for Image Recognition", Proc. CVPR, 2016. <a href="https://doi.org/10.1109/CVPR.2016.90">doi:10.1109/CVPR.2016.90</a></li>
</ol>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-12-07</pubDate>
    </item><item>
    <title>The machine learning pipeline</title>
    <link>https://research.adfoucart.be/machine-learning-pipeline</link>
    <description><p>Solutions to machine learning tasks can generally be represented in the form of a pipeline, going from the "input", which is the raw data that it works on, to its "output", which will be its solution to the task.</p>

<p>More accurately, there will typically be two pipelines: one for learning, and one for "production", when the trained model can be applied to its task on new data.</p>

<p>Let’s go back to the "face detection" problem from <a href="/every-ml-algorithm">my previous post</a>. The learning pipeline, which would be applied to all the examples in the training dataset, could look something like this:</p>


  <img src='./img/ml-pipeline-1.png' class="img-rounded img-responsive" alt="Machine learning pipeline for training" width='800px' />
  Fig. 1 - A machine learning pipeline.


<p>Let's take it step by step.</p>

<p>First, we apply some pre-processing to the image. The goal of pre-processing is to make global transformations that reduce the signal of whatever we don't want to detect and increase the signal of what we want to detect. A common step would be noise reduction, or edge detection.</p>

<p>Then, we try to detect "candidates". This step is also very common in image processing tasks. The goal is to not train the model not on one large image with several faces in it, but on many small images with a maximum of one face, as it will make the problem less complex. We could just cut the image into small fragments, or we can first use a simple metric (for instance: sample regions from areas where some borders are present) to compute possible candidates.</p>

<p>Third, we can compute useful features on those fragments. Feature extraction means that we replace the pixel information by a set of vectors of statistics computed on these pixels. It could be the mean value, the standard deviation, but also more complex things like the gradient directions or the presence or absence of certain pre-determined patterns. After this step, we will have replaced our image by a set of vectors. In other words, we now have our data "points", each feature corresponding to a dimension in a possibly highly dimensional feature space. If we only have two features, we could represent every candidate as a point in 2D, as we did with the "movie" data from the previous post.</p>

<p>Step 4 is what we generally think of when talking about "training a machine learning model": fitting the parameters of the model based on the features and the available annotations (e.g. does the fragment contain a face or not?)</p>

<p>Finally, we would probably have to apply some post-processing to the output of the model. This would mean taking the prediction for each fragment, putting the "positive" predictions back on the original image, and possibly using some additional rules to clean up the results. For instance: if we have two fragments which are mostly overlapping, we could merge them into one single face.</p>

<p>The parameters of the machine learning model are those optimized in step 4. But we also have to train the hyper-parameters of the whole pipeline: what type of pre-processing, how do we determine the candidates, which features we choose, what post-processing rules, etc...</p>

<p>Once the model is trained, it could then be used "in production" with the adapted pipeline on previously unseen images:</p>

<ol>
<li>Apply same pre-processing.</li>
<li>Get candidates.</li>
<li>Extract features.</li>
<li>For each fragment, apply the model to the extracted features and predict if it contains a face or not.</li>
<li>Apply post-processing.</li>
</ol>

<p>So, what's different about "Deep Learning"?</p>

<p>In this case, the main things that would change if we used a Deep Learning model would be to remove the pre-processing step, and to merge the feature extractions and model optimization steps into one, as illustrated below.</p>


  <img src='./img/ml-pipeline-2.png' class="img-rounded img-responsive" alt="Deep learning pipeline for training" width='600px' />
  Fig. 2 - A deep learning pipeline.


<p>The reason for that is that Deep Learning method work best by reducing the "human bias" from the equation. If pre-processing is needed, the Deep Learning model should be able to find it by itself. If some features are more useful than others, the model should decide it find them from the raw data, not use those that the researcher thought about including in its tests.</p>

<p>It's important to note that, even with a Deep Learning method, there are still a lot of hyper-parameters outside of the network itself. We will very rarely see, for instance, a Deep Learning solution where we just dump the entire images in the network along with the position of all the faces and expect the network to learn from that. This would make the problem extremely complex, and therefore require a very large network, a huge number of examples (and, therefore, a huge amount of time-consuming annotations), and an unrealistic amount of time to train, unless you're Google.</p>

<p>That's why it’s always dangerous to reduce an algorithm only to the specific architecture of neural network used: many choices are made along the way which influence the end result just as much.</p>

<p>In the next article, we will look at the building blocks of most Deep Learning models: the different layers that form deep neural networks.</p> </description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-11-30</pubDate>
    </item><item>
    <title>Every machine learning algorithm</title>
    <link>https://research.adfoucart.be/every-ml-algorithm</link>
    <description>

<p>One of the main things that interested us when our laboratory started working on Deep Learning for digital pathology was to determine which parts of "the deep learning pipeline" actually mattered. Many different Deep Neural Networks have been developed over the years, and in different competitions we often see multiple solutions based on networks which are very similar (or identical) obtaining largely different results [<a id='dlb1-ref1-a' href='#dlb1-ref1'>1</a>].</p>

<p>Networks which win high profile challenges are considered state-of-the-art and become wildly used, but it’s not always clear how much of the success (or failure) of a given solution can be attributed to the network itself.</p>

<p>So, before we get into that, let’s take a moment to go through the basics. As we have said before, Deep Learning is a subset of machine learning. Let’s therefore review first what are the main elements of a "Machine Learning" solution, what constitutes the "machine learning pipeline", and what are the building blocks of a Deep Neural Network for image analysis.

<h3>Every machine learning problem and algorithm</h3>

<p>Any machine learning problem is characterised by a Dataset, a Task, and an Evaluation metric.</p>

<p>The Dataset can take many different forms. It is the "knowledge base" from which we will train our algorithms. In the kind of problems of interest to us here, it will generally be a set of images which may be associated with a set of annotations. The annotations can be very precise (we know what every pixel of the image represent), very imprecise (we know that there is a particular object of interest somewhere in the image), or inexistent. Depending on this level of annotation, we will talk about supervised, weakly supervised, semi supervised or unsupervised datasets, but that’s a topic for later.</p>

<p>The Task is what we want to do with the dataset. Do we want to recognize faces in pictures of a crowd? To count the number of nuclei in a microscopy image? To determine the size and position of a tumour in an MRI?</p>

<p>Finally, the Evaluation metric describes how we are going to decide what a "good result" means for solving our tasks. This may be a harder problem than it looks. Let's take as an example a "face detection" task. The task consists in detecting the location of all the faces in a given image. Let's now say that we have three competing algorithms which obtain the results shown below. Which one is the best?</p>


  <img src='./img/face-detect.png' class="img-rounded img-responsive" alt="Face detection results" width='100%' /></a>
  Fig. 1 - Three different results on the same image. Which is the best? <a href="https://www.pexels.com/photo/family-of-four-walking-at-the-street-2253879/">Original photo</a> by Emma Bauso from Pexels.


<p>The first one only detected 75% of the faces, but each predicted face is correct. The second detected all faces but added a false prediction. The last one detected all the faces, but the localization is imprecise. The goal of a good evaluation metric is to provide "a quantitative measure of its performance"[<a id='dlb1-ref2-a' href='#dlb1-ref2'>2</a>], but different measures will favour different types of performances. If we require a very large overlap between the "true" region of the face and the detected region, the third algorithm will be last, while if we allow approximative localization and simply measure which faces were detected (the sensitivity) and if there were no false detections (the specificity), it may end up being first.</p>

<p>Any machine learning algorithm, conversely, is characterized by a Model with parameters and hyper-parameters, and by an Optimization method.</p>

<p>The Model defines the relationship between the input of the algorithm (the data) and its output. Let's go back to our example from our <a href="/what-is-deep-learning">"So... what’s Deep Learning?"</a> post. We were trying to determine if we will like a movie based on the percentage of comedy and the percentage of violence it contains.</p>

<p>One of the most basic models is the linear model. If \(p_c\), \(p_v\) are the percentages of comedy and violence, a linear model would search for a line with the equation \(y = a \times p_c + b \times p_v + c\) that best separates the "movie we like" from the "movie we dislike". \(a\), \(b\) and \(c\) are the parameters of the model. It's what the model will have to learn using the dataset. Running a simple linear classifier such as the <a href="https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.RidgeClassifier.html#sklearn.linear_model.RidgeClassifier">Ridge Classifier</a> from the scikit-learn Python library on some fake, generated data would give us something like the figure below.</p>


  <img src='./img/movie-linear.png' class="img-rounded img-responsive" alt="Results of linear model" width='300px' /></a>
  Fig. 2 - Results of the RidgeClassifier on fake "movie" data.


<p>The hyper-parameters are all the choices that were made while designing the algorithm. A linear model is, for instance, a specific case of a polynomial model, so we could say that the degree of the polynomial model is an hyper-parameter that we have chosen to set at 1.</p>

<p>The Optimization method is the algorithm that we will use to learn the parameters. There are many different ways to go about this, but the main idea is that we will try to adapt the parameters step by step until they are in a position where we can't improve the results anymore. At each step, the algorithm evaluates if each parameter needs to be increased or decreased to get a better result, and by how much, according to the training data. When any change in the parameters seems to give worse results, it stops.</p>

<p>This also requires us to define what "improving" the results mean, which comes back to the question of the evaluation metric. Without going to deep here, it's worth noting that a good evaluation metric for determining if an algorithm is the best at solving a Task isn't necessarily a good metric for optimizing the parameters. As a quick example, any metric that takes a significant amount of time to compute will not be practical for the learning process, as "learning" will generally involve doing millions or billions of such evaluations.</p>

<p>In general, a model with more parameters is said to be more complex. It can potentially solve problems which are more difficult, but at the cost of being harder to train.</p>


  <img src='./img/movie-poly3.png' class="img-rounded img-responsive" alt="Results of polynomial model" width='300px' /></a>
  Fig. 3 - Results of the third degree polynomial model on fake "movie" data.


<p>To end this chapter, I briefly want to touch on two important notions related to that complexity: underfitting and overfitting. A model is said to be underfitting if it isn't complex enough to accurately represent the data. It is overfitting if it is so complex that it can match the data almost perfectly, which seems like a good thing at first, but is in fact quite problematic. It means that the model has memorized all the examples from the dataset, but isn't capable of generalizing to new examples.</p>

<p>As an illustration, I have trained three different classifiers on the fake "movie" dataset:</p>

<ul>
	<li>a linear model (trying to fin a straight line that best separates the data), shown in fig. 2 above.</li>
	<li>a third-degree polynomial model (trying to fit a curve of the form \(a \times x^3 + b \times x^2 + c \times x + d\), shown in fig. 3 above.</li>
	<li>a “nearest neighbour” model (attributing the class of the nearest point in the training dataset), shown in fig. 4 below.</li>
</ul>


  <img src='./img/movie-nn.png' class="img-rounded img-responsive" alt="Results of nearest neighbour model" width='300px' /></a>
  Fig. 4 - Results of the nearest neighbour model on fake "movie" data.


<p>Figures 2-3-4 show the result of the training, with the frontier between the two classes. In the linear and polynomial model, we see that there are points which are on the wrong side of the border. The nearest neighbour, on the other hand, perfectly follows the training set. But is it better?</p>

<p>In the table below, we can see the accuracy (percentage of correct classification) of each model on the training data, as well as on new, previously unseen examples from the same dataset:</p>

<table>
	<tr>
		<th>Accuracy</th>
		<th>Training</th>
		<th>Test</th>
	</tr>
	<tr>
		<td>Linear</td>
		<td>87%</td>
		<td>94%</td>
	</tr>
	<tr>
		<td>Polynomial</td>
		<td>89%</td>
		<td>96%</td>
	</tr>
	<tr>
		<td>Nearest Neighbour</td>
		<td>100%</td>
		<td>89%</td>
	</tr>
</table>

<p>The nearest neighbour may better represent the training data, but its performance on new examples is the worst of the three. This is a clear sign of overfitting. The difference between the linear and third-degree polynomial is small, but as increasing the complexity improves both the training and the testing accuracy, it's likely that the linear model is underfitting the dataset.</p>

<p>In the next article, we will build on that to look at what constitutes a machine learning pipeline.</p>

<h3>References</h3>
<ol>
    <li id='dlb1-ref1'>[<a href='#dlb1-ref1-a'>&#8593;</a>] G. Aresta et al., "BACH: Grand challenge on breast cancer histology images", Medical Image Analysis 56 (2019). <a href="https://doi.org/10.1016/j.media.2019.05.010">doi:10.1016/j.media.2019.05.010</a></li>
    <li id='dlb1-ref2'>[<a href='#dlb1-ref2-a'>&#8593;</a>] I. Goodfellow, Y. Bengio, A. Courville (2016) "Deep Learning", MIT Press <a href="https://www.deeplearningbook.org/">https://www.deeplearningbook.org/</a></li>
</ol>
</description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-11-25</pubDate>
    </item><item>
    <title>MITOS12: learning from bad challenge design</title>
    <link>https://research.adfoucart.be/mitos12-learning-bad-challenge-design</link>
    <description><p>The ICPR 2012 mitosis detection competition - usually called MITOS12 - proved that Deep Learning was the way to go for mitosis detection, and was influential in introducing Deep Learning into the world of Digital Pathology. It was also a flawed challenge, and there is a lot to learn from the mistakes that were made in its design. To understand those mistakes, we first need to look at what the goal of a "computer vision challenge" is.</p>

<h3>1. How do we know if a computer vision algorithm is better than another?</h3>

<p>Let's say you are a researcher who just developped a new algorithm for recognizing the species of a bird from a photograph in a natural setting. Now, you want to publish your research. In order to convince reviewers that your algorithm is interesting, however, you not only have to prove that it works, but you also have to prove that it improves on the current state-of-the-art.</p>

<p>If you are not a big fan of ethics, here's what you do: you find a set of test images where your algorithm works really, really well. Then, you implement other algorithm, but without caring too much if you do it exactly right. You test all algorithms on your test set and, surprise, you are the best! This may be very helpful in getting published, but of course it also doesn't tell us anything useful.</p>

<p>This is of course an exageration... but not so far from the truth. Even if you do care for ethics, it's very hard to implement other people's methods - especially if they didn't publish the code - and to find the best parameters for every method on your dataset. A very popular way to solve this problem is to use benchmarks and challenges.</p>

<p>The idea of the benchmark is fairly simple. Let's take our "bird recognition" problem: at one point in time, someone publishes a large collection of annotated bird images. Everyone who works on the problem of "bird recognition" can test their algorithm on the same data, which means that you can directly compare your method to what others have published. Challenges are similar, except that there is usually a "time limit" component: someone publishes a bird dataset, and tell everyone interested to submit their method before a given date. Then, everyone is evaluated at the same time (ideally on previously unreleased test images), and the results are published. This ensures a certain fairness in the comparison, as everyone plays by the same rules.</p>

<p>While many challenges only attract a few participants and are quickly forgotten, some have become true references by the computer vision community. The <a href="http://host.robots.ox.ac.uk/pascal/VOC/">PASCAL Visual Object Classes</a> challenge, for instance, ran between 2005 and 2012, with researchers tasked with recognizing objects from up to 20 different classes (see figure below). Starting in 2010, the <a href="http://image-net.org/challenges/LSVRC/">ImageNet Large Scale Visual Recognition Challenge</a> (ILSVRC, often simply referred to as "ImageNet") has become the reference for "generic computer vision task".</p>


  <img src='./img/voc11-classes.png' class="img-rounded img-responsive" alt="Classes from the PASCAL VOC challenge" width='700px' />
  Classes from the PASCAL VOC challenge (<a href="http://host.robots.ox.ac.uk/pascal/VOC/voc2011/index.html">source</a>)


<p>Many Deep Learning algorithms have become famous through their ImageNet performances, such as the previously mentioned AlexNet [<a id='fimc-ref1-a' href='#fimc-ref1'>1</a>] (winner in 2012), Google's "Inception" network [<a id='fimc-ref2-a' href='#fimc-ref2'>2</a>] (winner in 2014), or Microsoft's "ResNet" [<a id='fimc-ref3-a' href='#fimc-ref3'>3</a>] (winner in 2015).</p>

<p>Challenges entered the world of digital pathology around 2012. Quoting Geert Litjens (emphasis mine):</p>

<blockquote>
    <p>The introduction of grand challenges in digital pathology has fostered the development of computerized digital pathology techniques. The challenges that evaluated existing and new approaches for analysis of digital pathology images are: EM segmentation challenge 2012 for the 2D segmentation of neuronal processes, mitosis detection challenges in ICPR 2012 and AMIDA 2013, GLAS for gland segmentation and, CAMELYON16 and TUPAC for processing breast cancer tissue samples.</p>
    G. Litjens et al [<a id='fimc-ref4-a' href='#fimc-ref4'>4</a>]
</blockquote>

<p>MITOS12 [<a id='fimc-ref5-a' href='#fimc-ref5'>5</a>] was one of the first computer vision contest with a specific digital pathology task. Let's take a closer look at it.</p>

<h3>2. MITOS12: The Challenge</h3>

<p>First of all, why do we want to count mitosis? I'll let the experts explain (emphasis mine):</p>

<blockquote>
    <p>Mitotic count is an important parameter in breast cancer grading as it gives an evaluation of the aggressiveness of the tumor. However, consistency, reproducibility and agreement on mitotic count for the same slide can vary largely among pathologists. An automatic tool for this task may help for reaching a better consistency, and at the same time reducing the burden of this demanding task for pathologists.</p>
    L. Roux et al [<a id='fimc-ref5-a' href='#fimc-ref5'>5</a>]
</blockquote>


  <a href='./img/mitos12-a00-02.bmp'><img src='./img/mitos12-a00-02-small.jpg' class="img-rounded img-responsive" alt="MITOS12 example region" width='500px' /></a>
  There are 3 mitosis in this image from the MITOS12 dataset, according to the pathologist who annotated it. Good luck finding them! (click on the image for a full-scale version)


<p>The following process was used to create the challenge:</p>



<ol>
    <li>A team of the pathology department of the Pitié-Salpêtrière Hospital selected five slides stained with H&E, each coming from a different patient.</li>
    <li>Each slide was scanned with three different scanners (Aperio, Hamamatsu and a multispectral microscope).</li>
    <li>One pathologist manually annotated, in 50 regions (10 per slides), all mitotic cells. There were 326 annotated mitosis in the dataset. An example of a full region is shown above.</li>
    <li>The 50 regions were split into a "training set" of 35 regions and a "test set" of 15 regions.</li>
    <li>The training set (images and annotations) was released in November 2011, and different teams started developping and training their algorithms.</li>
    <li>The test set (images only) was provided in August 2012, and participating teams sent their predictions</li>
    <li>All participants were evaluated using the following metric:
        <ul>
            <li>Count the number of "True Positive" (TP), meaning the number of mitotic cells that were correctly predicted by the algorithm.</li>
            <li>Count the number of "False Negatives" (FN), meaning the number of mitotic cells that were not detected by the algorithm.</li>
            <li>Count the number of "False Positive" (FP), meaning the number of non-mitotic cells that were incorrectly detected as mitosis by the algorithm.</li>
            <li>Compute the F1 Score, defined as \(2 TP \over 2 TP + FN + FP\). The <a href="https://en.wikipedia.org/wiki/F1_score">F1 score</a> is a very common metric for classification tasks, which gives equal importance to the "precision" (are "positive predictions" accurate?) and "recall" (do we find all "positive examples"?).</li>
        </ul></li>
    <li>The organizer published a <a href="http://ludo17.free.fr/mitos_2012/results.html">ranking</a> of the algorithms according to this metric.</li>
</ol>

<p>The MITOS12 results were computed separately on the images from the three different scanners. Most teams only submitted results for the "Aperio" scanner. The best results on that dataset were achieved by Dan Cireşan's team, IDSIA [<a id='fimc-ref6-a' href='#fimc-ref6'>6</a>]. Their algorithm correctly detected 70 of the 100 mitosis in the test set, with only 9 false detections, for a winning F1 score of 0.7821, the runner-ups achieving scores of 0.7184 and 0.7094.</p>

<h3>3. What is wrong with those results?</h3>

<p>There are numerous problems with the challenge design, which were actually acknowledged by the authors in this paragraph from the Discussion of their article:</p>

<blockquote>
    <p>An improved version of this successful challenge will involve a much larger number of mitosis, images from more slides and multiple pathologists’ collaborative/cooperative annotations. Besides, some slides will be dedicated to test only without any HPF of these slides included in the training data set.</p>
    L. Roux et al [<a id='fimc-ref5-a' href='#fimc-ref5'>5</a>]
</blockquote>

<p>There are four different issues here, so let's take them one at a time.</p>

<p>1) The small number of mitosis, with only around 200 mitosis in the training set, is certainly a problem. As mitosis can have a very variable appearance, it is unlikely that all of the possible "morphologies" of mitotic cells will be represented in the dataset. Machine Learning algorithm will therefore not be able to detect them.</p>

<p>2) The small number of slides is even more problematic. More importantly, a small number of slides from a small number of patients. Having more patients mean more diversity, and less risk of a bias in the dataset. For instance, if a patient happens has more mitosis than the others (because she has a more malignant cancer), and also happens to have some independent morphological characteristics in her breast tissue, it is very likely that the algorithm will pick up those independent features and use them to predict the presence of mitotic cells, even if those features would be completely meaningless for other patients.</p>

<p>3) The fact that only one pathologist annotated the slides is also worrying. In the introduction to the challenge, they list as a reason for needing automatic counting the lack of inter-pathologist agreement on mitotic count, citing a study by Christopher Malon and his colleagues [<a id='fimc-ref7-a' href='#fimc-ref7'>7</a>]. In that study, three different pathologists were asked to classify 4204 nuclei as mitotic or non-mitotic. Pathologists were allowed to put "Maybe" as an answer. Even excluding those "maybe" from the comparison (and therefore comparing only cases where both pathologists were "reasonably certain" of their choice), the agreement between two pathologists was at most 93.5% of "same classification", and in the worst case at 64.7%. That same study also compares each pathologist against the "majority label". The F1 score of a pathologist against the majority, using the data from that study, vary between 0.704 and 0.997. In the MITOS12 challenge results, the top 3 teams are all within that range.</p>

<p>Now that doesn't mean that those three teams are necessarily better than, or even as good as pathologists. It's not fair to compare them on different datasets. But the point is, the difference in performance between the top teams in the challenge is small enough that a different annotator might have led to a completely different ranking.</p>

<p>4) The last problem acknowledged by Roux's publication is the worse one, at least in terms of methodology: they didn't split the training set and the test set properly. Ideally, when we test an algorithm, we want to make sure that it is capable of handling new cases. The best way to do that if to have a test set as independent as possible from the training set.</p>

<p>In biomedical images, that typically means: testing on other slides, taken from other patients, if possible taken with another acquisition machine. Changing more variables mean that the results are a lot more meaningfull. The best algorithms would then have to be those that really learned ways to describe the object of interest - in this case the mitosis. In her master thesis in our lab, Elisabeth Gruwé tested the same algorithm using either the "official" training-set / test-set split from the contest, and then using a "correct" split, by putting one patient aside for testing and training on the four others. The results on the official split were close to those of the three winning teams (0.68), the results on the correct split were significantly worse (0.54).</p>

<h3>4. Does it matter?</h3>

<p>Does the ranking matter? In terms of visibility, probably. The methods proposed by challenge winners tend to be copied, modified, adapted, and become the norm, while runner-ups may be completely ignored... even if their results are functionally equivalent. If we look at the publications of the three MITOS12 winning methods, a certain trend is visible. The winner, Cireşan's team's article [<a id='fimc-ref6-a' href='#fimc-ref6'>6</a>], was published in the biggest biomedical imagining conference and has been cited more than a thousand times (according to Google Scholar). The runner-up, Humayun Irshad from the University of Grenoble, was published in a good journal [<a id='fimc-ref8-a' href='#fimc-ref8'>8</a>] and has been cited about 150 times. The third, Ashkan Tashk and a team from Shiraz University of Technology, was published in the proceedings of an obscure iranian conference [<a id='fimc-ref9-a' href='#fimc-ref9'>9</a>] and cited about 30 times.</p>

<p>Now the rankings are not the only explanation for this difference in visibility, and the number of citation is not a direct reflection of the influence of a paper. Dan Cireşan was part of a well-established research team with Jurgen Schmidhuber, a Deep Learning pioneer. Humayun Irshad's thesis director was Ludovic Roux, the organizer of the challenge, which is kind of a problem of its own, but ensured that he got some visibility in the follow-up articles. Ashkan Tashk and the iranian team certainly didn't have the same recognition beforehand - or after.</p>

<p>Two years later, an extended version of the challenge was proposed at the ICPR 2014 conference, <a href="https://mitos-atypia-14.grand-challenge.org/">MITOS-ATYPIA 14</a>. It provided more data, and the annotations were made by two different pathologist, with a third one looking at all cases were the first two were in disagreement. The data included a confidence score for each mitosis based on the agreement or disagreement of the pathologists... and it was correctly split at the patient level. In 2016, Hao Chen and his Hong Kong team published their results on both the 2012 and 2014 datasets [<a id='fimc-ref10-a' href='#fimc-ref10'>10</a>]. On the 2012 dataset, they achieve a F1 score of 0.788, "beating" Cireşan's entry. They also beat all other existing publications on the 2014 dataset... with a score of 0.482. Comparing the two datasets, they say:</p>

<blockquote>
    <p>One of the most difficult challenges in [the 2014] dataset is the variability of tissue appearance, mostly resulted from the different conditions during the tissue acquisition process. As a result, the dataset is much more challenging than that in 2012 ICPR.</p>
    H. Chen et al [<a id='fimc-ref10-a' href='#fimc-ref10'>10</a>]
</blockquote>

<p>But from a purely machine learning perspective, this doesn't sound quite right. Yes, the increased variability in the test set is more challenging, but the increased variability in the training set should help the algorithms. The huge drop in performance is likely to be in a large part due to the incorrect setup of the 2012 challenge. The 2014 edition, however, attracted a lot less participants, and didn't get the same visibility... probably because the results were, for obvious reasons, a lot worse.</p>

<h3>References</h3>

<ol>
    <li id='fimc-ref1'>[<a href='#fimc-ref1-a'>&#8593;</a>] A. Krizhevsky, I. Sutskever, G. Hinton (2012) "ImageNet Classification with Deep Convolutional Neural Networks", Communications of the ACM, 60(6), DOI: <a href="https://doi.org/10.1145/3065386">10.1145/3065386</a></li>
    <li id='fimc-ref2'>[<a href='#fimc-ref2-a'>&#8593;</a>] C. Szegedy et al (2015) "Going deeper with convolutions", CVPR 2015, DOI: <a href="https://doi.org/10.1109/CVPR.2015.7298594">10.1109/CVPR.2015.7298594</a></li>
    <li id='fimc-ref3'>[<a href='#fimc-ref3-a'>&#8593;</a>] K. He, X. Zhang, S. Ren, J. Sun (2015) "Deep residual learning for image recognition", CVPR 2016, DOI: <a href="https://doi.org/10.1109/CVPR.2016.90">10.1109/CVPR.2016.90</a></li>
    <li id='fimc-ref4'>[<a href='#fimc-ref4-a'>&#8593;</a>] G. Litjens et al. (2017) "A survey on deep learning in medical image analysis", Medical Image Analysis, 42, 60-88, DOI: <a href="https://doi.org/10.1016/j.media.2017.07.005">10.1016/j.media.2017.07.005</a></li>
    <li id='fimc-ref5'>[<a href='#fimc-ref5-a'>&#8593;</a>] L. Roux et al. (2013) "Mitosis detection in breast cancer histological images An ICPR 2012 contest", Journal of Pathology Informatics, 4, 1, DOI: <a href="https://doi.org/10.4103/2153-3539.112693">10.4103/2153-3539.112693</a></li>
    <li id='fimc-ref6'>[<a href='#fimc-ref6-a'>&#8593;</a>] D. Cireşan, A. Giusti, L. Gambardella, J. Schmidhuber (2013) "Mitosis Detection in Breast Cancer Histology Images with Deep Neural Networks", Proceedings of MICCAI 2013 in Lecture Notes in Computer Science, 8150, 411-418, DOI: <a href="https://doi.org/10.1007/978-3-642-40763-5_51">10.1007/978-3-642-40763-5_51</a></li>
    <li id='fimc-ref7'>[<a href='#fimc-ref7-a'>&#8593;</a>] C. Malon et al (2012) "Mitotic figure recognition: agreement among pathologists and computerized detector", Analytical Cellular Pathology , 35(2), 97-100, DOI: <a href="https://doi.org/10.3233/ACP-2011-0029">10.3233/ACP-2011-0029</a></li>
    <li id='fimc-ref8'>[<a href='#fimc-ref8-a'>&#8593;</a>] H. Irshad (2013) "Automated mitosis detection in histopathology using morphological and multi-channel statistics features", Journal of Pathology Informatics, 4: 10, DOI: <a href="https://doi.org/10.4103/2153-3539.112695">10.4103/2153-3539.112695</a></li>
    <li id='fimc-ref9'>[<a href='#fimc-ref9-a'>&#8593;</a>] A. Tashk, M. Helfroush, H. Danyali, M. Akbarzadeh (2013) "An automatic mitosis detection method for breast cancer histopathology slide images based on objective and pixel-wise textural features classification", Proceedings of CIKT, DOI: <a href="https://doi.org/10.1109/IKT.2013.6620101">10.1109/IKT.2013.6620101</a></li>
    <li id='fimc-ref10'>[<a href='#fimc-ref10-a'>&#8593;</a>] H. Chen, Q. Dou, X. Wang, J. Qin, P. Heng (2016) "Mitosis Detection in Breast Cancer Histology Images via Deep Cascaded Networks.", AAAI Conference on Artificial Intelligence, <a href="https://www.aaai.org/ocs/index.php/AAAI/AAAI16/paper/viewPaper/11788">AAAI Publications</a></li>
</ol></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-03-09</pubDate>
    </item><item>
    <title>A timeline of Deep Learning in Digital Pathology</title>
    <link>https://research.adfoucart.be/timeline-deep-learning-in-digital-pathology</link>
    <description><h3>1. Who was first?</h3>

<p>In 2017, Geert Litjens and his colleagues from Radboud University published a comprehensive survey of deep learning methods in medical image analysis [<a id='tdldp-ref1-a' href='#tdldp-ref1'>1</a>]. It's possible that they missed some early papers that could qualify as "Deep Learning": as I've <a href="/what-is-deep-learning">written before</a>, the boundaries of what is or isn't Deep Learning are unclear, and articles written before 2012 are unlikely to use that terminology. If such articles exist, however, they have been forgotten in the large pile of never-cited research that fails to be picked-up by Google Scholar, Scopus, or other large research databases. Litjens' survey therefore remains the reference on the matter.</p>

<p>So who was first to use "Deep Learning in Digital Pathology"? The turning point seems to come from the <a href="https://link.springer.com/book/10.1007/978-3-642-40763-5">MICCAI 2013</a> conference in Nagoya, with two pioneering articles: Angel Cruz-Roa's automated skin cancer detection [<a id='tdldp-ref2-a' href='#tdldp-ref2'>2</a>], and Dan Cireşan's mitosis detection in breast cancer [<a id='tdldp-ref3-a' href='#tdldp-ref3'>3</a>].</p>

<p>Cruz-Roa uses H&E stained images from skin tissue, to try to automatically determine if there is a malignant cancer in the sample. As illustrated in the figure below, the difference between cancer and non-cancer is based on morphological criteria which are very difficult to define.</p>


  <img src='./img/cruzroa13.png' class="img-rounded img-responsive" alt="Example of BCC histopathology" width='500px' />
  Example of H&E images from cancer and non-cancer skin tissue, taken from [<a id='tdldp-ref2-a' href='#tdldp-ref2'>2</a>]


<p>Their algorithm produces both a classification (cancer or not) at the image level, and what they call a "digital staining", which is basically a probability map of where the cancer regions are (see figure below). This is a very important feature for machine learning methods in biomedical imaging, related to the concept of interpretability. A machine learning algorithm which only produces a "diagnosis", but is unable to "explain" how this diagnosis came to be, cannot be trusted. I will most certainly come back to that idea later: the "reasoning" that machine learning algorithms (deep or not) make are sometimes more a reflexion of biases and artefacts in the data that was used to train it than of an understanding of the pathology. Having an output which includes an explanation of the diagnosis is therefore essential to control whether any "weird stuff" is happening.</p>


  <img src='./img/cruzroa13b.png' class="img-rounded img-responsive" alt="Output of Cruz-Roa's algorithm" width='500px' />
  Output of Cruz-Roa's algorithm, taken from [<a id='tdldp-ref2-a' href='#tdldp-ref2'>2</a>]


<p>Cireşan also uses H&E images, taken from breast cancer tissue. I'll let him introduce the problem:</p>

<blockquote>
    <p>Mitosis detection is very hard.</p>
    Cireşan, Giusti, Gambardella & Schmidhuber [<a id='tdldp-ref3-a' href='#tdldp-ref3'>3</a>]
</blockquote>

<p>Mitosis - the process of cell multiplication - is a relatively rare event, meaning that, in the images which are available to train the algorithm, only a very small fraction will be part of a nuclei, and an even smaller fraction will be part of a nuclei going through mitosis. In addition to being rare, the appearance of the cell nucleus will be very different depending of which stage of the mitosis the cell is currently experiencing. To get an idea of how difficult the task is, we can just look at these examples from the article (click on the image for a larger version):</p>


  <a href='./img/ciresan13.png'><img src='./img/ciresan13.png' class="img-rounded img-responsive" alt="Mitosis and non-mitosis examples" width='500px' /></a>
  Examples of mitosic and non-mitosic cells, with the "mitosis probability" given by Cireşan's algorithm and the "true classification", taken from [<a id='tdldp-ref3-a' href='#tdldp-ref3'>3</a>]


<p>Cireşan's results were far from perfect, but they were impressive enough to be a milestone in the domain: as a winner of the "ICPR 2012 mitosis detection competition", it got a lot of attention... despite the many methodogical issues with the competition itself, which is the topic for another post.</p>

<h3>2. Invasion of the Deep Learners</h3>

<p>By showing that Deep Learning was a way to get good results on digital pathology tasks, Cireşan and Cruz-Roa opened up the floodgates. Litjens lists many different applications in the subsequent years: bacterial colony counting, classification of mitochondria, classification and grading of different types of cancer, detection of metastases... Mostly on H&E images, but also sometimes using immunohistochemistry, Deep Learning invaded the domain.</p>

<p>A few highly influential works that I would like to mention here, and that I will probably write about more later:</p>

<ul>
    <li>In 2015, Olaf Ronneberger, Philipp Fischer and Thomas Brox introduced the U-Net network architecture [<a id='tdldp-ref4-a' href='#tdldp-ref4'>4</a>], winning challenges in cell segmentation and cell tracking at the ISBI 2015 conference. This particular architecture is now probably the most widely used in biomedical imaging.</li>
    <li>In 2016, Andrew Janowczyk and Anant Madabhushi published their Deep Learning for digital pathology tutorial [<a id='tdldp-ref5-a' href='#tdldp-ref5'>5</a>], a very practical article on how to approach various use cases in digital pathology, with well-annotated datasets that they also made public. For reasearchers in the field, this is probably one of the best available resource to get started.</li>
    <li>In 2017, Korsuk Sirinukunwattana published the results of the "GlaS" gland segmentation in colon histology images challenge [<a id='tdldp-ref6-a' href='#tdldp-ref6'>6</a>]. This challenge was influential in two ways: first, by providing a high-quality dataset of colon histology images with very accurate annotations, and second by demonstrating how much Deep Learning had penetrated the field of digital pathology. Of the 6 methods deemed good enough to be described in the results article, 5 used deep learning approaches.</li>
</ul>

<h3>3. Looking forward</h3>

<p>After these pioneering works, the future of the field may seem a little dull. If we have deep neural network that work for most digital pathology tasks... what is there left to do?</p>

<p>Fortunately, finding a good "deep learning network" is only a part of the "digital pathology pipeline". Everything that comes around the network - from the constitution of the datasets to the way the results are evaluated - is often more important to the final result. That is going to be a large part of what I will write about in future posts. Questions surrounding how data from challenges and data from real-world application may differ, questions about the way we evaluate algorithms, about how we declare winners and losers in ways that may not always reflect how useful the algorithms really are. For that, a good starting point will be to take a closer look at the aforementioned <a href="http://ludo17.free.fr/mitos_2012/index.html">MITOS12 challenge</a> from ICPR 2012.</p>

<h3>References</h3>

<ul type='1'>
    <li id='tdldp-ref1'>[<a href='#tdldp-ref1-a'>&#8593;</a>] G. Litjens et al. (2017) "A survey on deep learning in medical image analysis", Medical Image Analysis, 42, 60-88, DOI: <a href="https://doi.org/10.1016/j.media.2017.07.005">10.1016/j.media.2017.07.005</a></li>
    <li id='tdldp-ref2'>[<a href='#tdldp-ref2-a'>&#8593;</a>] A. Cruz-Roa, J. Ovalle, A. Madabhushi, F. Osorio (2013) "A Deep Learning Architecture for Image Representation, Visual Interpretability and Automated Basal-Cell Carcinoma Cancer Detection", Proceedings of MICCAI 2013 in Lecture Notes in Computer Science, 8150, 403-410, DOI: <a href="https://doi.org/10.1007/978-3-642-40763-5_50">10.1007/978-3-642-40763-5_50</a></li>
    <li id='tdldp-ref3'>[<a href='#tdldp-ref3-a'>&#8593;</a>] D. Cireşan, A. Giusti, L. Gambardella, J. Schmidhuber (2013) "Mitosis Detection in Breast Cancer Histology Images with Deep Neural Networks", Proceedings of MICCAI 2013 in Lecture Notes in Computer Science, 8150, 411-418, DOI: <a href="https://doi.org/10.1007/978-3-642-40763-5_51">10.1007/978-3-642-40763-5_51</a></li>
    <li id='tdldp-ref4'>[<a href='#tdldp-ref4-a'>&#8593;</a>] O. Ronneberger, P. Fischer, T. Brox (2015) "U-Net: Convolutional Networks for Biomedical Image Segmentation",  Proceedings of MICCAI 2015 in Lecture Notes in Computer Science, 9351, 234-241, DOI: <a href="https://doi.org/10.1007/978-3-319-24574-4_28">10.1007/978-3-319-24574-4_28</a></li>
    <li id='tdldp-ref5'>[<a href='#tdldp-ref5-a'>&#8593;</a>] A. Janowczyk, A. Madabhushi (2016) "Deep learning for digital pathology image analysis: a comprehensive tutorial with selected use cases", Journal of Pathology Informatics, 7, 29, DOI: <a href="https://doi.org/10.4103/2153-3539.186902">10.4103/2153-3539.186902</a></li>
    <li id='tdldp-ref6'>[<a href='#tdldp-ref6-a'>&#8593;</a>] K. Sirinukunwattana et al (2017) "Gland Segmentation in Colon Histology Images: The GlaS Challenge Contest", Medical Image Analysis, 35, 489-502, DOI: <a href="https://doi.org/10.1016/j.media.2016.08.008">10.1016/j.media.2016.08.008</a></li>
</ul></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-03-02</pubDate>
    </item><item>
    <title>So... what's Deep Learning?</title>
    <link>https://research.adfoucart.be/what-is-deep-learning</link>
    <description><h3>1. Defining Deep Learning</h3>

<p>It seems like Deep Learning should have an easy, clear-cut definition. Yet... <a href="https://en.wikipedia.org/wiki/Deep_learning">Wikipedia</a>, on this topic, displays a remarkable example of circular citation - or <a href="https://xkcd.com/978/">Citogenesis</a>, as the always-relevant XKCD would put it. The Wikipedia definition is a "summary" of five definitions from <a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/DeepLearning-NowPublishing-Vol7-SIG-039.pdf">a Microsoft Research paper</a>, most of which are themselves taken from earlier versions of the same Wikipedia article.</p>

<p>The most direct definition from a reputable source that I could find is probably from the "Deep Learning" Nature Review of AI-superstars Yann LeCun, Yoshua Bengio and Geoffrey Hinton (emphasis mine):</p>

<blockquote>
    <p>Deep learning allows computational models that are composed of multiple processing layers to learn representations of data with multiple levels of abstraction.</p>
    LeCun, Bengio & Hinton [<a id='widl-ref1-a' href='#widl-ref1'>1</a>]
</blockquote>

<p>In a slightly more convoluted way, Ian Goodfellow, Yoshua Bengio (again) and Aaron Courville, in their "Deep Learning" book, introduce the topic this way (emphasis mine):</p>

<blockquote>
    <p>The true challenge to artificial intelligence proved to be solving the tasks that are easy for people to perform but hard for people to describe formally—problems that we solve intuitively, that feel automatic, like recognizing spoken words or faces in images. (...) A solution is to allow computers to learn from experience and understand the world in terms of a hierarchy of concepts, with each concept deﬁned through its relation to simpler concepts.</p>
    Goodfellow, Bengio & Courville [<a id='widl-ref2-a' href='#widl-ref2'>2</a>]
</blockquote>

<p>These definitions basically boil down to: it's AI, with machine learning, with layers. So... What's machine learning? And, while we're at it, what is AI?</p>


  <img src='./img/dl-nvidia.png' class="img-rounded img-responsive" alt="NVIDIA AI Timeline" width='500px' />
  Timeline of AI, Machine Learning and Deep Learning, by NVIDIA. [<a href="https://blogs.nvidia.com/blog/2016/07/29/whats-difference-artificial-intelligence-machine-learning-deep-learning-ai/">source</a>]


<h3>2. What is Artificial Intelligence?</h3>

<p>The history of Artificial Intelligence as a practical, computer-science-related field of research, goes back to the early days of computers themselves, around and right after World War II. The "ultimate goal" of AI, as illustrated in Alan Turing's best-known paper [<a id='widl-ref3-a' href='#widl-ref3'>3</a>], was to create a computer which could be - at least in specific conditions - indistinguishable from a human.</p>

<p>This, unsurprisingly, is a difficult task. In fact, this particular goal, which is now generally referred to as "Artificial General Intelligence", is still mostly the domain of science-fiction.</p>

<p>One of the early avenues of research in AI, in the tradition of "trying to replicate human intelligence", was the artificial neuron. As early as 1943, McCulloch and Pitts [<a id='widl-ref4-a' href='#widl-ref4'>4</a>] proposed a way to represent neurons in a mathematical model which could be replicated on a computer. They were followed by many others, but while their research was interesting, it proved to be largely impractical. Neural networks, quite simply, did not work. Artificial (General) Intelligence seemed altogether impossible. If AI as it was conceived couldn't be done, the next best thing was to change the definition of AI to something more forgiving.</p>

<p>In "Artificial Intelligence: foundations of computational agents" [<a id='widl-ref5-a' href='#widl-ref5'>5</a>], Poole & Mackworth propose such a definition. AI, in their view, studies computational agents (which are agents whose actions and decisions can be implemented in a physical device, like a computer) that act intelligently, which means that it has actions appropriate for its circumstances and its goals, is flexible to changing environments and changing goals, and learns from experience.</p>

<p>This places any artificial intelligence in the context of task or problem solving. The job of an AI is not to be "like a human", but to have "human-like" (or better-than-human) capabilities in one or several specific tasks.</p>

<p>An interesting aspect of AI as defined by Poole & Mackworth is the capacity to learn from experience. Machine Learning is a subset of AI dealing with this particular aspect of "intelligence": how can a machine learn from experience?</p>

<h3>3. Machine Learning</h3>

<p>To understand the basic idea of Machine Learning, it's interesting to look at one of its earliest algorithms, "Nearest Neighbor", with a version described already in 1951 [<a id='widl-ref6-a' href='#widl-ref6'>6</a>].</p>

<p>Let's take an example. Imagine that you are a Big Streaming Company, and you want your AI to decide which movies or series in your catalog you should recommend to a particular user. Let's assume that you actually want to recommend something that the user will like, and not just something that you want to promote. For every movie or tv series, you have two pieces of information: how much violence, and how much comedy there is, as a percentage of the total runtime of the movie. For everything that the user has already seen, you also know (don't ask how) if he liked it or not.</p>

<p>You could therefore represent all of the movies that the user has seen on a nice graph, like this:</p>


  <img src='./img/nn-algo-ex.png' class="img-rounded img-responsive" alt="Very important movie data" width='500px' />
  How do you know if you will like the next movie?


<p>What you do know is that you take the movie that you want to recommend, and you also put it on the same graph. Then, you check if the closest point is something that the user liked or not. If not, you don't recommend the movie.</p>

<p>Obviously, this is a ridiculous example, but the main idea is there: the algorithm uses past experience to predict new behaviour. Of course, this will work a lot better if you have more data, and if you have better ways of describing this data.</p>

<p>There are many, many, more complex, more accurate algorithms in Machine Learning. But in the end, they fundamentally do the exact same thing: put all of the data (the "past experience") into some space that describes is as best as possible, and then find in that space a "Rule" that best predicts what to do with any future event. This rule can usually be as simple as a straight line (or to be precise in the more-than-two-dimensions case, an hyperplane) dividing the "space" in two, or be an intricate function with millions of parameters able to model any boundary shape, as in modern artificial neural networks.</p>

<h3>4. Deep Learning and the neural networks comeback</h3>

<p>While diverse machine learning algorithms such as Decision Trees, Support Vector Machines, and many others were being developped, the "artificial neural network" world was not completely inactive. One of the major issues with neural networks - how to efficiently "train" them with new examples - was vastly improved upon in 1986 with the backpropagation algorithm [<a id='widl-ref7-a' href='#widl-ref7'>7</a>], which is still used today. In 1989, Yann LeCun and his team used it in what is now considered one of the first practical application of "modern" artificial neural networks, to recognize hand-written ZIP codes for the US Postal system [<a id='widl-ref8-a' href='#widl-ref8'>8</a>].</p>

<p>In 1997, IBM's Deep Blue beat world champion Gary Kasparov in a six-game chess match [<a id='widl-ref9-a' href='#widl-ref9'>9</a>]. In terms of public perception, this certainly gave AI enthusiasts a boost. Deep Blue, however, was an "Intelligence" only if you used the most forgiving definition. It didn't learn anything, it didn't reason anything: it was a pure, brute-force mechanism. Deep Blue simply took the current situation of the game, and computed all possible outcomes for all possible moves, for the next 10 to 20 moves. It did use "previous experience", in the form of thousands of previously played human-vs-human grandmaster games, to determine what a "winning move" was. But in the end, it mostly relied on the fact that chess is a game with fixed rules and a finite amount of outcomes. It worked because it had more processing power than what was previously available, not because it was innovative.</p>

<p>LeCun's success put neural networks back on the map, but they were still a curiosity. In most applications, they were impractical, took way too long to train, and didn't usually perform better than other machine learning approaches. But with the 21st century came two game changers in the machine learning world: Big Data and fast GPUs. Big Data - the ability to store huge amount of data on everything, thanks to cheap hard drives - gave us the ability to improve machine learning in general. Fast GPUs made training larger, more useful neural networks a reality. Quoting Dan Cireşan and his colleagues in 2010:</p>

<blockquote>
    <p>All we need to achieve this best result so far are many hidden layers, many neurons per layer, numerous deformed training images, and graphics cards to greatly speed up learning.</p>
    Cireşan, Meier, Gambardella & Schmidhuber [<a id='widl-ref10-a' href='#widl-ref10'>10</a>]
</blockquote>

<p>Around the same time, the idea that "larger" neural networks were actually "deeper" neural networks, with neurons organized in "layers", each layer connected to the next starting from the raw data all the way up to the output, became common usage.</p>

<p>By 2012, the achievements of "Deep" neural networks became impossible to ignore. On ImageNet, the largest visual object recognition challenge, Alex Krizhesvsky's AlexNet [<a id='widl-ref11-a' href='#widl-ref11'>11</a>] dominated that year's field. Deep Learning approaches have since then consistently beaten "classical" machine learning methods on about everything. Most predominantly, it has become the standard solution for computer vision and language processing. In the world of AI, Deep Learning is now the law of the land.

<h3>Conclusion?</h3>

<p>To summarize:</p>

<ul>
    <li>Artificial Intelligence describes any (artificial) system that take (intelligent) actions based on its environment.</li>
    <li>Machine Learning describes a subset of AI which use past events (the "learning" dataset) to create a model of "how to react to new events" (the "decision rule").</li>
    <li>Deep Learning describes a subset of Machine Learning where the model is learned in a "layered" manner, with simple rules learned from the raw data, which feed into more complex rules, which feed into more complex rules, etc, etc, until you get to more abstract concepts. To take a computer vision example: from pixels, to shapes and colors, to eyes and noses, to recognizable faces.</li>  
</ul>

<p>These definitions are fuzzy. The boundaries between Deep Learning and "non-deep" Machine Learning are unclear, as are sometimes the boundaries between Machine Learning and "old fashioned AI". That's fine: we don't need every method to fit into a well-defined box.</p>

<p>All right. Now that we have defined what <a href="/what-is-digital-pathology">Digital Pathology</a> and Deep Learning are, the next question will be: how has Deep Learning been applied to Digital Pathology?</p>

<h3>References</h3>

<ul type='1'>
    <li id='widl-ref1'>[<a href='#widl-ref1-a'>&#8593;</a>] Y. LeCun, Y. Bengio, G. Hinton (2015) "Deep Learning", Nature, 521, 436-444, DOI: <a href="https://doi.org/10.1038/nature14539">10.1038/nature14539</a></li>
    <li id='widl-ref2'>[<a href='#widl-ref2-a'>&#8593;</a>] I. Goodfellow, Y. Bengio, A. Courville (2016) "Deep Learning", MIT Press <a href="https://www.deeplearningbook.org/">https://www.deeplearningbook.org/</a></li>
    <li id='widl-ref3'>[<a href='#widl-ref3-a'>&#8593;</a>] A. Turing (1950) "Computer machinery and intelligence", MIND, 59, 433-60, DOI: <a href="https://doi.org/10.1093/mind/LIX.236.433">10.1093/mind/LIX.236.433</a></li>
    <li id='widl-ref4'>[<a href='#widl-ref4-a'>&#8593;</a>] W. McCulloch, W. Pitts (1943). "A logical calculus of the ideas immanent in nervous activity." Bulletin of Mathematical Biophysics, 7, 115–133.</li>
    <li id='widl-ref5'>[<a href='#widl-ref5-a'>&#8593;</a>] D. Poole, A. Mackworth (2017) "Computational Intelligence:  foundations of computational agents (Second Edition)", <a href="https://artint.info/">available online</a></li>
    <li id='widl-ref6'>[<a href='#widl-ref6-a'>&#8593;</a>] E. Fix, J. Hodges (1951) "Discriminatory Analysis - Nonparametric Discrimination: Consistency Properties", USAF School of Aviation Medicine - University of California, Berkeley, <a href="https://apps.dtic.mil/docs/citations/ADA800276">available online</a></li>
    <li id='widl-ref7'>[<a href='#widl-ref7-a'>&#8593;</a>] D. Rumelhart, G. Hinton, R. Williams (1986) "Learning representations by back-propagating errors.", Nature, 323, pp 533-536, DOI: <a href="https://doi.org/10.1038/323533a0">10.1038/323533a0</a></li>
    <li id='widl-ref8'>[<a href='#widl-ref8-a'>&#8593;</a>] Y. LeCun, B. Boser, J. Denker, D. Henderson, R. Howard, W. Hubbard, et al. (1989) "Back-propagation applied to handwritten zip code recognition.", Neural Computation, 1(4), pp 541-551, DOI: <a href="https://doi.org/10.1162/neco.1989.1.4.541">10.1162/neco.1989.1.4.541</a></li>
    <li id='widl-ref9'>[<a href='#widl-ref9-a'>&#8593;</a>] <a href="https://en.wikipedia.org/wiki/Deep_Blue_(chess_computer)">Deep Blue (chess computer) on Wikipedia</a>, seen on Feb 12th, 2020.</li>
    <li id='widl-ref10'>[<a href='#widl-ref10-a'>&#8593;</a>] D. Cireşan, U. Meier, L. Gambardella, J. Schmidhuber (2010) "Deep Big Simple Neural Nets Excel on Hand-written Digit Recognition", Neural Computation, 22(12), pp 3207-3220, DOI: <a href="https://doi.org/10.1162/NECO_a_00052">10.1162/NECO_a_00052</a></li>
    <li id='widl-ref11'>[<a href='#widl-ref11-a'>&#8593;</a>] A. Krizhevsky, I. Sutskever, G. Hinton (2012) "ImageNet Classification with Deep Convolutional Neural Networks", Communications of the ACM, 60(6), DOI: <a href="https://doi.org/10.1145/3065386">10.1145/3065386</a></li>
</ul></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-02-24</pubDate>
    </item><item>
    <title>So... what's Digital Pathology?</title>
    <link>https://research.adfoucart.be/what-is-digital-pathology</link>
    <description><p>Let's start from the beginning. When I started my thesis, the topic we settled on was "Deep Learning in Digital Pathology". It's vague - but that was kind of the point. Deep Learning and Digital Pathology were both recent trends at the time, so trying to look at what could be done with it in general seemed like a good idea.</p>
<p>We have two parts in "Deep Learning in Digital Pathology". The first one, Deep Learning, is where I have spent most of my thesis. That's the part that concerns me as a biomedical engineer specialized in image analysis, and where I can contribute the most. The second part, however, is just as important. It's the application, what we want to use Deep Learning for: Digital Pathology. I am certainly not an expert in Digital Pathology - and even less of an expert in not-digital histopathology - but understanding what the methods I'm going to develop may be used for does seem like a good idea, so let's briefly get into it.</p>

<h3>1. Histopathology</h3>

<p>The goal of histopathology is to examine human tissue (usually taken from a tumour or some other possibly diseased area) under a microscope, to formulate a diagnosis or to get a better understanding of a disease. The process, in short, is as follows:</p>
<ol type='a'>
    <li>Cut a small bit of tissue from someone's body (typically during surgery, or a biopsy)</li>
    <li>"Fix" it in formalin (or freeze it) so the tissue keeps it shape, and then embed it in a paraffin block.</li>
    <li>Cut the block in very thin slices using a microtome, which is basically a very small and precise meat slicer.</li>
    <li>Stain the tissue to make whatever we are looking for more visible.</li>
    <li>Have a trained pathologist examine the resulting slide under a microscope to get a diagnosis.</li>
</ol>

<p>Why do we have to stain the tissue? Because cells are mostly water, water tends to be transparent [citation needed], and transparent things are hard to look at with visible light. Fortunately, some chemical pigments have properties which are very useful for pathologists. For instance, in the late 19th and early 20th century, it was discovered that we could use hematoxylin to stain the nuclei of cells in blue, and eosin to stain the cytoplasm in pink [<a id='widp-ref1-a' href='#widp-ref1'>1</a>].</p>

<p>This produces "Hematoxylin & Eosin" - H&E - images like this one below, where the structure of the tissue is easy to analyse for the pathologist:</p>


  <img src='./img/glas-he.bmp' class="img-rounded img-responsive" alt="H&E stained image." />
  H&E stained image, from [<a id='widp-ref2-a' href='#widp-ref2'>2</a>].


<p>Slightly more recently, we realized that we could use the properties of antigens and antibodies to get some more specific staining. The idea is this: in our body, some cells - antibodies - are designed to specifically bind to some proteins - antigens - as a defence mechanism to produce an immune response. We can "hack" this process by binding a staining agent to an antigen and therefore "highlight", in the tissue, places where the related antibodies are present. This method is called immunohistochemistry, or IHC. For instance, in the image below, we have the same part of the tissue stained with H&E on one side, and with an IHC marker (anti-pan-cytokeratin, to be precise) on the other. The IHC marker highlights the cells which are part of a tumour, which is a rather useful information to have in histopathology.</p>


  <img src='./img/he-vs-ihc.png' class="img-rounded img-responsive" alt="(left) H&E versus (right) IHC-stained images, from colorectal cancer tissue." />
  (left) H&E versus (right) IHC-stained images, from colorectal cancer tissue.


<h3>2. Digital pathology</h3>

<p>So where does the "digital" part fit in all this?</p>

<p>The problem with the process above is that it requires the trained pathologist to be physically in front of the microscope, with the slide in it. There are a number of drawbacks to this. One is that it's hard to get a second opinion from a specialist from somewhere else. Another is that comparing a tissue to, for instance, another sample taken some months or years before requires finding the physical slide in the archives of the hospital.</p>

<p>How do we solve that? With digital scanners. Very expensive, very high resolution, very precise digital scanners. The entire slide can be scanned at multiple levels of magnification to produce Gigapixel images, which can be viewed on a computer. The pathologist can then access the image in a "virtual microscope".</p>

<p>And once you start to have the slides as digital objects, you open the door to many possibilities, from tools to easily annotate the slides (for instance, for teaching purposes, or simply to quickly document the reasoning behind a diagnosis) to automated analysis of certain aspects of the tissue. In particular, some quantitative analysis are very hard to do for a human expert in an objective manner (like evaluating "what percentage of the tissue shows this marker?"), yet relatively easy (or at least possible) to do for a well-designed algorithm.</p>

<blockquote><p>Digital image acquisition are becoming commonplace, and associated image analysis solutions are viewed by most as the next critical step in automated histological analysis.</p>Laoighse Mulrane [<a id='widp-ref3-a' href='#widp-ref3'>3</a>]</blockquote>

<p>In the more than 10 years since Mulrane's paper, there has indeed been a wide range of image analysis applications in digital pathology. And, in more recent years, as in most image analysis applications, one type of strategy seems to have very quickly surpassed all others: Deep Learning.</p>

<p>I guess that's a teaser for the "next episode"?</p>

<h3>References</h3>

<ul type='1'>
    <li id='widp-ref1'>[<a href='#widp-ref1-a'>&#8593;</a>] Michael Titford (2009) "Progress in the Development of Microscopical Techniques for Diagnostic Pathology", Journal of Histotechnology, 32:1, 9-19, DOI: <a href="https://doi.org/10.1179/his.2009.32.1.9">10.1179/his.2009.32.1.9</a></li>
    <li id='widp-ref2'>[<a href='#widp-ref2-a'>&#8593;</a>] K. Sirinukunwattana et al (2017), "Gland segmentation in colon histology images: The glas challenge contest", Med. Image Anal., vol. 35, pp. 489–502. DOI: <a href="https://doi.org/10.1016/j.media.2016.08.008">10.1016/j.media.2016.08.008</a></li>
    <li id='widp-ref3'>[<a href='#widp-ref3-a'>&#8593;</a>] L. Mulrane et al (2008), "Automated image analysis in histopathology: a valuable tool in medical diagnostics", Expert Rev. Mol. Diagno., 8 (6), pp. 707-725. DOI: <a href="https://doi.org/10.1586/14737159.8.6.707">10.1586/14737159.8.6.707</a></li>
</ul></description>
    <author>adrien@adfoucart.be</author> 
    <pubDate>2020-02-17</pubDate>
    </item></channel></rss>